【算法】数组中出现次数超过一半的数字

版权声明:本文为博主原创学习笔记,如需转载请注明来源。 https://blog.csdn.net/SHU15121856/article/details/82627606

面试题39:数组中超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1, 2, 3, 2, 2, 2, 5, 4, 2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。

解法1

数字超出一半,随机选中该数字的概率就很大。可以随机选一个数字,然后用快排的Partition划分一下,小的放左边,大的放右边,如果当前在中间位置,就说明已经找到了排序后中间位置的那个数(中位数),就一定是要找的超过一半的数字。否则就比较一下位置,向左找向右找,递归这个过程。

Partition划分

作者写的这个划分,其实是把小的放左边,大于等于的放右边。

#include <stdlib.h>
#include "Array.h"
#include <exception>

//在min和max之间随机找一个数 
int RandomInRange(int min, int max) {
    //即随机数对距离取整,再加上最小数使其落在这个范围 
    int random = rand() % (max - min + 1) + min;
    return random;
}

//交换两指针所对应的值 
void Swap(int* num1, int* num2) {
    int temp = *num1;
    *num1 = *num2;
    *num2 = temp;
}

//在start和end中随机找一个数,在长为length的data数组中以此下标为界
//划分整个数组,使比它(所对应的数)小的数都在其左边,>=它的在右边 
int Partition(int data[], int length, int start, int end) {
    //输入合法性检查 
    if(data == nullptr || length <= 0 || start < 0 || end >= length)
        throw new std::exception();
    //随机取该范围内的一个划分点 
    int index = RandomInRange(start, end);
    //将其和最后一个位置的数进行交换(因为从边界的位置开始划分比较方便)
    Swap(&data[index], &data[end]);

    /*接下来作者的操作需要画图理解!*/

    int small = start - 1;//这只是方面后面在循环里都做++ 
    //从前到后遍历除最后位置之外的所有位置 
    for(index = start; index < end; ++ index) {
        //如果发现比分隔数小的数字 
        if(data[index] < data[end]) {
            ++ small;//这时small才+1
            //注意index随着循环总是+1,但是small遇到一些>=分隔数的数字时
            //就会暂且落后一个身位,在>=分隔数的数字前面
            //当下一次遇到比分隔数小的数字时,就会执行++small踩上来 
            if(small != index)//这里去除无用操作,相等时即指向同一个比分隔数小的数字 
                //交换两个数,使得让small指针受阻的第一个>=分隔数的数字换过来 
                Swap(&data[index], &data[small]);//取而代之的是一个比分隔数小的数字 
            //每次交换完成之后,相当于为small指针破开了一个向前走的阻碍
            //这个阻碍就是>=分隔数的数字
        }
        //因为这个遍历会遍历完整个(除最后一个分隔数之外)数组
        //相当于去拿后面的小数字去砸阻碍small的大石头,能砸掉多少是多少
        //整个遍历完成后,small前面仍然会有(也可以没有)大石头(指>=分隔数的数字)
        //但是这些石头里不会再夹杂任何比分隔数小的数字了 
    }

    //所以,至此,数组里的情况是[小..小(small)][石..石(index)][分隔数(end)] 
    //(当然也可以没有小于分隔数的数字,或者没有>=分隔数的数字,这些情况都能通过) 
    //现在,small指向最后一个比分割数字小的数字,index指向分隔数前一个数字 

    ++ small;//small向前走一步踩在接下来第一个石头上 
    Swap(&data[small], &data[end]);//将其和最后的分隔数交换 

    //现在数组情况:[小..小][分隔数(small)][石..石(index)石(end)]

    return small;//返回分隔数所在位置下标 
}

这没什么问题,但其实这样做以后,并不能达到作者在书上说的”小的放左边,大的放右边”这种效果,因为超过一半的数字很多,不止一个,使用上面的划分函数,那么与基准数相等的会被放到右边。所以即使找到了一堆超过一半的数字,还是会落在最左边的一个上,它往往不是中位数。

所以这个解法其实真正找的过程比较累,我在代码里加了一些输出看一下。

寻找超过一半的数字
#include<bits/stdc++.h>
#include "../Utilities/Array.h"
using namespace std;

//全局变量,用于指示是否出错
bool g_bInputInvalid = false;

//检查数组是否合法
bool CheckInvalidArray(int* numbers, int length) {
    g_bInputInvalid = false;
    if(numbers == nullptr && length <= 0)//数组不合法时
        g_bInputInvalid = true;//同样在这个全局变量上做出指示

    return g_bInputInvalid;
}

//确认number在长为length的numbers数组中是否超过一半
bool CheckMoreThanHalf(int* numbers, int length, int number) {
    //统计number在数组中出现的次数
    int times = 0;
    for(int i = 0; i < length; ++i) {
        if(numbers[i] == number)
            times++;
    }

    bool isMoreThanHalf = true;
    //如果没有超过一半
    if(times * 2 <= length) {
        g_bInputInvalid = true;//在全局变量上做出指示
        isMoreThanHalf = false;
    }

    return isMoreThanHalf;
}

//把上面两部分单独写到一个函数里可以和方法解耦,多个方法不用重复代码 

//====================方法1====================
int MoreThanHalfNum_Solution1(int* numbers, int length) {
    if(CheckInvalidArray(numbers, length))
        return 0;
    //长度的一半,数组中位数出现的位置 
    int middle = length >> 1;
    int start = 0;
    int end = length - 1;
    //随机划分一下,返回划分后的坐标位置 
    int index = Partition(numbers, length, start, end);
    cout<<"划分位置是"<<index<<",值是"<<numbers[index]<<endl;
    //只要划分后不在中位数位置,就一直循环
    //注意:这个Partition每次划分将>=它的都放在右边
    //所以右边的数往往会很多,特别是在这个题的这种大量一样数字的输入情况下 
    //并且在后续的递归中,这种情况也完全不会变好
    //不妨输出一下每次划分的位置看一下这种有点蠢的查找方式,, 
    while(index != middle) {
        if(index > middle) {//如果比中位数大 
            end = index - 1;//就继续在左边找 
            index = Partition(numbers, length, start, end);
            cout<<"划分位置是"<<index<<",值是"<<numbers[index]<<endl;
        } else {//如果比中位数小 
            start = index + 1;//就继续在右边找 
            index = Partition(numbers, length, start, end);
            cout<<"划分位置是"<<index<<",值是"<<numbers[index]<<endl;
        }
    }

    int result = numbers[middle];//最终结果就是划分在中间位置时(中位数)
    //最后要检查一下是不是确实超过一半,花O(n)时间不增加总时间复杂度 
    if(!CheckMoreThanHalf(numbers, length, result))
        result = 0;
    return result;
}

int main() {
    int numbers[]={1,2,3,2,2,2,2,2,2,2,2,2,5,4,2};
    MoreThanHalfNum_Solution1(numbers,15);
    return 0;
}

最终的运行结果:
这里写图片描述
最后面连续一串2,当数组很大的时候,这种情况更严重。

解法2

超过半数以上,说明它自己就比其它数字加起来还多,所以以其一可以敌全部,可以打擂台赛。

从第一个数字开始上擂台,遍历整个数组,挑战者数字和它相同,就把它分数+1,挑战者数字和它不同,就把它分数-1,减到0就换擂主,最终的赢家就是要找的数字。

//====================方法2====================
int MoreThanHalfNum_Solution2(int* numbers, int length) {
    if(CheckInvalidArray(numbers, length))//检查数组合法性 
        return 0;

    int result = numbers[0];//擂主一开始是第一个数字 
    int times = 1;//分数是1
    //遍历剩下的所有挑战者 
    for(int i = 1; i < length; ++i) {
        if(times == 0) {//如果分数掉到0了
            result = numbers[i];//有新的挑战者上台直接当擂主
            times = 1;//刚上台分数肯定是1 
        } else if(numbers[i] == result)//如果和擂主同族(数字一样) 
            times++;//分数+1,相当于给擂主加HP 
        else//如果不同族(数字不一样) 
            times--;//掉一分,相当于擂主花1HP打掉1HP的挑战者 
    }

    //最终检查一下找到的数字是不是确实超过一半 
    if(!CheckMoreThanHalf(numbers, length, result))
        result = 0;

    return result;
}

猜你喜欢

转载自blog.csdn.net/SHU15121856/article/details/82627606