编程之美--发帖水王

转载:http://blog.csdn.net/v_july_v/article/details/6890054


第二十一~二十二章:出现次数超过一半的数字,最短摘要的生成



    第一个问题来自编程之美上,Tango是微软亚洲研究院的一个试验项目,如图1所示。研究院的员工和实习生们都很喜欢在Tango上面交流灌水。传说,Tango有一大“水王”,他不但喜欢发帖,还会回复其他ID发的每个帖子。坊间风闻该“水王”发帖数目超过了帖子总数的一半。如果你有一个当前论坛上所有帖子(包括回帖)的列表,其中帖子作者的ID也在表中,你能快速找出这个传说中的Tango水王吗?

                                          图1  Tango

   

第二十一章、发帖水王及其扩展

  第一节、74.数组中超过出现次数超过一半的数字

题目:数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。 

分析:编程之美上也有这道题,不过它变换了题目的表述形式,即是如本文前言所述的寻找发帖水王的问题。

    ok,咱们来解决上述这道题,以微软面试100题第74题的阐述为准(本程序员编程艺术系列就是按照之前整理的微软100题一题一题展开而来的)。

一个数组中有很多数,现在我们要找出这个数组中那个超过出现次数一半的数字,怎么找呢?大凡当我们碰到某一个杂乱无序的东西时,我们人的内心本质期望是希望把它梳理成有序的。所以,我们得分两种情况来讨论,无序和有序:

  1. 如果无序,那么我们是不是可以先把数组中所有这些数字先进行排序,至于选取什么排序方法则不在话下,最常用的快速排序ON*logN)即可。排完序呢,直接遍历。在遍历整个数组的同时统计每个数字的出现次数,然后把那个出现次数超过一半的数字直接输出,题目便解答完成了。总的时间复杂度为ON*logN+N)。
  2. 但各位再想想,如果是有序的数组呢或者经过上述由无序的数组变成有序后的数组呢?是否在排完序ON*logN)后,真的还需要再遍历一次整个数组么?我们知道,既然是数组的话,那么我们可以根据数组索引支持直接定向到某一个数。我们发现,一个数字在数组中的出现次数超过了一半,那么在已排好序的数组索引的N/2处(从零开始编号),就一定是这个数字。自此,我们只需要对整个数组排完序之后,然后直接输出数组中的第N/2处的数字即可,这个数字即是整个数组中出现次数超过一半的数字,总的时间复杂度由于少了最后一次整个数组的遍历,缩小到ON*logN)。
  3.  然不论是上述思路一的ON*logN+N),还是思路二的ON*logN),时间复杂度并无本质性的改变。我们需要找到一种更为有效的思路或方法。既要缩小总的时间复杂度,那么就用查找时间复杂度为O1),事先预处理时间复杂度为ON)的hash。哈希表的键值(Key)为数组中的数字,值(Value)为该数字对应的次数。然后直接遍历整个hash表,找出每一个数字在对应的位置处出现的次数,输出那个出现次数超过一半的数字即可。
  4. Hash表需要ON)的开销空间,且要设计hash函数,还有没有更好的办法呢?我们可以试着这么考虑,如果每次删除两个不同的数(不管是不是我们要查找的那个出现次数超过一半的数字),那么,在剩下的数中,我们要查找的数(出现次数超过一半)出现的次数仍然超过总数的一半。通过不断重复这个过程,不断排除掉其它的数,最终找到那个出现次数超过一半的数字。这个方法,免去了上述思路一、二的排序,也避免了思路三空间ON)的开销,总得说来,时间复杂度只有ON),空间复杂度为O1),不失为最佳方法。

    或许,你还没有明白上述思路4的意思,举个简单的例子吧,如数组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 ,还能运用上述思路么?额,别急,请看下文思路5。

      5.   咱们根据数组的特性进一步考虑@zhedahht: 数组中有个数字出现的次数超过了数组长度的一半。也就是说,有个数字出现的次数比其他所有数字出现次数的和还要多。
    因此我们可以考虑在遍历数组的时候保存两个值:一个是数组中的一个数字,一个是次数。当我们遍历到下一个数字的时候,如果下一个数字和我们之前保存的数字相同,则次数加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不同,则ntime减1变为零,;接下来,遍历到数字2,2与1不同,candidate保存数字2,且ntimes重新设为1;继续遍历到第4个数字1时,与2不同,ntimes减一为零,同时candidate保存为1;最终遍历到最后一个数字还是1,与我们之前candidate保存的数字1相同,ntime加一为1。最后返回的是之前保存的candidate为1。


    针对上述程序,我再说详细点,0,1,2,1,1

  1. i=0,candidate=0,nTimes=1;
  2. i=1,a[1]=0 != candidate,nTimes--,=0;
  3. i=2,candidate=2,nTimes=1;
  4. i=3,a[3] != candidate,nTimes--,=0;
  5. i=4,candidate=1,nTimes=1;
  6. 如果是0,1,2,1,1,1的话,那么i=5,a[5]=1=candidate,nTimes++,=2;......

    Ok,思路清楚了,完整的代码如下

  1. //改自编程之美 2010  
  2. Type Find(Type* a, int N)  //a代表数组,N代表数组长度  
  3. {  
  4.     Type candidate;  
  5.     int nTimes, i;  
  6.     for(i = nTimes = 0; i < N; i++)  
  7.     {  
  8.         if(nTimes == 0)  
  9.         {  
  10.             candidate = a[i], nTimes = 1;  
  11.         }  
  12.         else  
  13.         {  
  14.             if(candidate == a[i])  
  15.                 nTimes++;  
  16.             else  
  17.                 nTimes--;  
  18.         }  
  19.     }  
  20.     return candidate;   
  21. }  

   


  第二节、加强版水王:找出出现次数刚好是一半的数字    

    问题扩展@ BrainDeveloper:我们知道,水王问题:有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。
    所以,如果还运用上面的程序的话,那么只能返回我们要找的数字1的前一个数字,即遍历到1之前所保存的candidate2,试问如何让程序能返回我们需要的数字1呢?望读者思考之:
    程序经过修改后,如下(未经严格测试,如有误恳请指正):

读者反馈

    据本文读者 tenger_lee评论反应,上述程序有bug,因为若数组序列是:1 0 2 1 2 1,则无法得到正确结果。现修正如下(只是程序复杂度高达为O(N^2),还待后续改进与优化):

持续改进

    赤血红狐用了两个变量来记录水王,最终完整代码如下:
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. int Find(int* a, int N)  
  5. {  
  6.     int candidate1,candidate2;  
  7.     int nTimes1, nTimes2, i;  
  8.           
  9.     for(i = nTimes1 = nTimes2 =0; i < N; i++)  
  10.     {  
  11.         if(nTimes1 == 0)  
  12.         {  
  13.             candidate1 = a[i], nTimes1 = 1;  
  14.         }  
  15.         else if(nTimes2 == 0)  
  16.         {  
  17.               
  18.             candidate2 = a[i], nTimes2 = 1;  
  19.         }  
  20.         else  
  21.         {  
  22.             if(candidate1 == a[i])  
  23.                 nTimes1++;  
  24.             else if(candidate2 == a[i])  
  25.                 nTimes2++;  
  26.             else  
  27.             {  
  28.                 nTimes1--;  
  29.                 nTimes2--;  
  30.             }  
  31.         }  
  32.     }  
  33.     return nTimes1>nTimes2?candidate1:candidate2;  
  34. }  
  35.   
  36. int main()  
  37. {  
  38.     int a[4]={0,1,2,1};  
  39.     cout<<Find(a,4)<<endl;  
  40. //  int a[6]={1,0,2,1,2,1};  
  41. //  cout<<Find(a,6)<<endl;  
  42. }  

得到上面的本人根据了编程之美实现了扩展问题:
有3个发帖很多的ID,他们的发帖数目都超过了总数的1/4,你能从发帖ID列表中快速找到他们的ID吗?
代码实现如下:
package com.jtlyuan.shuiwang;
public class ShuiWang {
public int[] find(int[] id) {
int[] res = new int[3];
int nTime1 = 0;
int nTime2 = 0;
int nTime3 = 0;
for (int i = 0; i < id.length; i++) {
if ((nTime1 == 0) && (id[i] != res[1]) && (id[i] != res[2])) {
res[0] = id[i];
nTime1 = 1;
} else if ((nTime2 == 0) && (id[i] != res[0]) && (id[i] != res[2])) {
res[1] = id[i];
nTime2 = 1;
} else if ((nTime3 == 0) && (id[i] != res[0]) && (id[i] != res[1])) {
res[2] = id[i];
nTime3 = 1;
} else {
if (res[0] == id[i]) {
nTime1++;
continue;
} else {
nTime1--;
}
if (res[1] == id[i]) {
nTime2++;
continue;
} else {
nTime2--;
}
if (res[2] == id[i]) {
nTime3++;
continue;
} else {
nTime3--;
}
}
}
return res;
}
public static void main(String[] args) {
int[] id = { 1, 1, 1, 6, 2, 2, 3, 1, 3, 2, 3, 1, 1, 1, 6, 2, 2, 3, 1,
3, 2, 3, 1, 1, 1, 6, 2, 2, 3, 1, 3, 2, 3};
int[] result = new ShuiWang().find(id);
for (int i = 0; i < result.length; i++) {
System.out.print(result[i] + " ");
}
}
}
/*
 * 1 3 2
 */
题目可以扩展成有K个发帖很多的ID,他们的发帖数目都超过了总数1/(K+1),你能从发帖ID列表中快速找到他们的ID吗?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值