【编程之美】2.3寻找发帖水王(数组中出现次数超过一半的数字)


题目:

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


题目解析:

碰到这类题要考虑是否有序,无序的时候一种情况,有序的时候一种情况。


思路一(排序查找):

通常碰到无序的数组,会通过排序然后进行查找,这时通过快速排序O(nlogn)来实现。然后我们再遍历整个数组,统计每个数字出现的次数,找到出现次数超过一半的数字。总的时间复杂度为O(nlogn + n)。


思路二:

如果数组有序了,我们还有必要再进行遍历么?题目中说超过一半的数字,那么我们应该看arr[N/2]这个位置的数据即可。但时间复杂度仍然为O(nlogn)。


思路三(空间换时间):

除了将数组由无序变成有序是常识,通过空间换取时间解决复杂度问题也是常识。那么我们设法建立一个hash表,键值为数组中的数字,表值为出现的次数。然后遍历整个哈希表,输出超过一半对应的数字即可。


思路四(直接查找第k个数据):

毕竟我们用了空间去优化,也是不得已的事情。但还应该考虑还有没有更好的方法。既然我们知道要查找N/2位置的数据,也就转化成了查找第k个数据。对这个问题,有一套解决办法。利用快速选择来解决。

随机选取枢纽元,一趟partition后,如果枢纽元位置为k则退出,找到;如果小于k,就在有半部分继续找;如果大于k,就在左半部分继续找。

// Random Partition
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;
}

int Partition(int data[], int length, int start, int end)
{
    if(data == NULL || length <= 0 || start < 0 || end >= length)
        throw new std::exception("Invalid Parameters");

    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;
            if(small != index)
                Swap(&data[index], &data[small]);
        }
    }

    ++ small;
    Swap(&data[small], &data[end]);

    return small;
}


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);
    while(index != middle)
    {
        if(index > middle)
        {
            end = index - 1;
            index = Partition(numbers, length, start, end);
        }
        else
        {
            start = index + 1;
            index = Partition(numbers, length, start, end);
        }
    }
 
    int result = numbers[middle];
    if(!CheckMoreThanHalf(numbers, length, result))
        result = 0;

    return result;
}

这个代码中实现,递归有半段时没有将start-k,因为start和end都是基于原数组的“绝对位置”来表示的,递归传入的参数start可能不为0。


思路五:

利用题目的特点,我们可以试着这么考虑,如果每次删除两个不同的数(不管是不是我们要查找的那个出现次数超过一半的数字),那么,在剩下的数中,我们要查找的数(出现次数超过一半)出现的次数仍然超过总数的一半。通过不断重复这个过程,不断排除掉其它的数,最终找到那个出现次数超过一半的数字。这个方法,免去了排序,也避免了空间O(n)的开销,总得说来,时间复杂度只有O(n),空间复杂度为O(1),貌似不失为最佳方法。
举个简单的例子,如数组a[5] = {0, 1, 2, 1, 1};
很显然,若我们要找出数组a中出现次数超过一半的数字,这个数字便是1,若根据上述思路4所述的方法来查找,我们应该怎么做呢?通过一次性遍历整个数组,然后每次删除不相同的两个数字,过程如下简单表示:
0 1 2 1 1 =>2 1 1=>1
最终1即为所找。

但是数组如果是{5, 5, 5, 5, 1},还能运用上述思路么?很明显不能,咱们得另寻良策。


改进优化:

咱们根据数组的特性考虑到:

  • 数组中有个数字出现的次数超过了数组长度的一半。换句话说,有个数字出现的次数比其他所有数字出现次数的和还要多。
  • 因此我们可以考虑在遍历数组的时候保存两个值:一个是数组中的一个数字,一个是次数,当我们遍历到下一个数字的时候:
    • 如果下一个数字和我们之前保存的数字相同,则次数加1;
    • 如果下一个数字和我们之前保存的数字不同,则次数减1,我们需要保存下一个数字,并把次数重新设为1。

下面,举两个例子:

  • 第一个例子,假定数组为{5, 5, 5, 5, 1}

    不同的相消,相同的累积。遍历到第四个数字时,candidate 是5, nTimes 是4;遍历到第五个数字时,candidate 是5, nTimes 是3;nTimes不为0,那么candidate就是超过半数的。

  • 第二个例子,假定数组为{0, 1, 2, 1, 1}

    开始时,保存candidate是数字0,nTimes为1;遍历到数字1后,与数字0不同,则nTimes减1变为零;接下来,遍历到数字2,2与1不同,candidate保存数字2,且nTimes重新设为1;继续遍历到第4个数字1时,与2不同,nTimes减1为零,同时candidate保存为1;最终遍历到最后一个数字还是1,与我们之前candidate保存的数字1相同,nTimes加1为1。最后返回的是之前保存的candidate为1。

代码一:(不够健壮性)

//改自编程之美 2010  
Type Find(Type* a, int N)  //a代表数组,N代表数组长度  
{  
    Type candidate;  
    int nTimes, i;  
    for(i = nTimes = 0; i < N; i++)  
    {  
        if(nTimes == 0)  
        {  
            candidate = a[i], nTimes = 1;  
        }  
        else  
        {  
            if(candidate == a[i])  
                nTimes++;  
            else  
                nTimes--;  
        }  
    }  
    return candidate;   
} 

代码二(加入了判断条件):

#include <iostream>  
using namespace std;  
  
bool g_Input = false;  
  
int Num(int* numbers, unsigned int length)  
{  
    if(numbers == NULL && length == 0)  
    {  
        g_Input = true;  
        return 0;  
    }  
    g_Input = false;  
      
    int result = numbers[0];  
    int times = 1;  
    for(int i = 1; i < length; ++i)  
    {  
        if(numbers[i] == result)  
            times++;  
        else  
            times--;  
        if(times == 0)  
        {  
            result = numbers[i];  
            times = 1;  
        }  
    }  
      
    //检测输入是否有效。  
    times = 0;  
    for(i = 0; i < length; ++i)  
    {  
        if(numbers[i] == result)  
            times++;  
    }  
    if(times * 2 <= length)  
        //检测的标准是:如果数组中并不包含这么一个数字,那么输入将是无效的。  
    {  
        g_Input = true;  
        result = 0;  
    }      
    return result;  
}  
  
int main()  
{  
    int a[10]={1,2,3,4,6,6,6,6,6};  
    int* n=a;  
    cout<<Num(a,9)<<endl;  
    return 0;  
} 


题目扩展:加强版水王:找出出现次数刚好是一半的数字 

我们知道,水王问题:有N个数,其中有一个数出现超过一半,要求在线性时间求出这个数。那么,我的问题是,加强版水王:有N个数,其中有一个数刚好出现一半次数,要求在线性时间内求出这个数。

因为,很明显,如果是刚好出现一半的话,如此例: 0,1,2,1 :

遍历到0时,candidate为0,times为1
遍历到1时,与candidate不同,times减为0
遍历到2时,times为0,则candidate更新为2,times加1
遍历到1时,与candidate不同,则times减为0;我们需要返回所保存candidate(数字2)的下一个数字,即数字1。

题目分析:

方案一:

正好一半的话,我们也可以利用类似减去两个不同的数来实现,这里我们减去的是三个数,因为至少有三个数是不相同的(当然数组中只有两个数a和b各占一半)。那么接下来的问题,就转化成了找到超过一半的数字,又变成第一类问题了。


方案二:

根据上面的例子,最后我们可能会输出不是符合条件的数字,那么仔细分析的话,占一半的数字,只能在两个变量中出现:candidate和arr[n-1]。如果arr[n-1]不是占一半的数据key,那么candidate最后保持着key,另一种情况,就是arr[n-1]为key。我们遍历到最后,再遍历一趟判断一下是否arr[n-1]占据一半即可。

int Find(int* a, int N)  //a代表数组,N代表数组长度      
{      
    int candidate;      
    int nTimes, i;      
    for(i = nTimes = 0; i < N; i++)      
    {      
        if(nTimes == 0)      
        {      
            candidate = a[i], nTimes = 1;      
        }      
        else      
        {      
            if(candidate == a[i])      
                nTimes++;      
            else      
                nTimes--;      
        }      
    }      
    
    int cTimes = 0;    
    int candidate2 = a[N-1];    
    for(i = 0; i < N; i ++)    
    {    
        if(a[i] == candidate)    
        {    
            cTimes++;    
        }    
    }    
    
    return cTimes == N/2 ? candidate : candidate2;       
}    

再改进:

我们再遍历的过程中,让每一个数据与arr[n-1]比较,统计和arr[n-1]相同的数据,那么到最后就不用再遍历了,代码如下:

int MoreThanHalf(int a[], int N)  
{  
    int sum1 = 0;//最后一个元素的个数  
    int sum2 = 0;  
    int candidate;  
    int i;  
    for(i=0;i<N-1;i++)//扫描前N-1个元素  
    {  
        if(a == a[N-1])//判断当前元素与最后一个是否相等  
        sum1++;  
        if(sum2 == 0)  
        {  
             candidate = a;  
             sum2++;  
        }  
        else  
        {  
             if(a == candidate)  
                 sum2++;  
             else  
                 sum2--;  
        }  
     }  
  
     if((sum1+1) == N/2)  
         return a[N-1];  
     else  
         return candidate;  
} 




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值