题目如下:

有2N个QQ号码,其中有一个QQ号码出现的次数大于N次,请找出这个QQ号码。要求:时空复杂度尽可能低。

有的人看到题目后就开始做,貌似也能得到正确结果,但无法通过腾讯的面试,主要是算法时空复杂度非最优。

解法一.暴力排序

排序是最容易想到的一种方法。由于目标QQ号码出现的次数超过数组长度的一半,所以排序后直接取中间元素就行。

我们知道,基于比较的排序,时间复杂度能达到​​O(NlogN)​​, 但这不是最好的方法。

解法二.计数统计

既然题目的意思是求出这个次数超过一半QQ号码,那就对次数进行计数吧,直接搞个HashMap就行了。此时,时间复杂度是​​O(N)​​​, 但空间复杂度也是​​O(N)​​,也不是最优解。

而且要注意到,使用计数法的时候,漏用了题目中的信息:出现次数大于N。显然,这是不合理的。这就跟你高考数学一样,你发现有个条件居然没用到,而你把题目做出来了,那是很值得怀疑的。

解法三.摩尔投票

接下来,我们来介绍一种很巧妙的思路,即摩尔投票法,时间复杂度是​​O(N)​​​, 空间复杂度是​​O(1)​​。思路是怎样的呢?

为了求超过半数的QQ号码,我们来看一种现实中的投票场景,以投票选择班长为例:

班级要开始选班长

要求半数以上通过

选出来的就是班长

我们先来看一种简单的情况:假如只有2个人A和B竞争班长这一职位。那么,在大家投票后,我们从投票箱中取数后,可以这样统计:

  • 先抽一张票,如果是A,则记录当前胜利者为A,票数:1

  • 再抽一张票,如果是A,则记录当前胜利者为A,票数:2

  • 再抽一张票,如果是B,则抵消,记录当前胜利者为A,票数:1

  • 再抽一张票,如果是B,则抵消,记录无当前胜利者

  • 再抽一张票,如果是A,则记录当前胜利者为A,票数:1

如此循环,直到所有票统计完毕,最后有票的一方,就是胜利者,就是大家选出来的班长。这就是所谓的摩尔投票法。

如上只是两方进行PK, 那么,当候选人为多人时,也可以使用类似的思路,这里的前提条件就是:一定存在多余半数票的候选人。

当多方进行PK时,占据着半数以上票数的班长,可以任性地抵消任何人的票,而且其他人之间的相互抵消也不会撼动班长的位置。

有的朋友看到这里,可能觉得有点绕,那么,我们一起来看看程序,并打印关键步骤,有兴趣的朋友可以对照程序和结果理解下。

#include <iostream>
using namespace std;

int solution(int a[], int n)
{
int count = 0, value = a[0];
for(int i = 0; i < n; i++)
{
printf("log, i=%d, count=%d, value=%d\n", i, count, value);
if(count == 0)
{
value = a[i];
}

if(a[i] == value)
{
count++;
}
else
{
count--;
}
}

return value;
}


int main()
{
int a[]={6,2,5,3,4,2,2,2,2,5};
int n = sizeof(a) / sizeof(a[0]);
cout << solution(a, n) << endl;
return 0;
}

打印结果如下:

log, i=0, count=0, value=6
log, i=1, count=1, value=6
log, i=2, count=0, value=6
log, i=3, count=1, value=5
log, i=4, count=0, value=5
log, i=5, count=1, value=4
log, i=6, count=0, value=4
log, i=7, count=1, value=2
log, i=8, count=2, value=2
log, i=9, count=3, value=2
2