【编程之美学习笔记】2.3寻找发帖水王
坊间风闻“水王”发帖数目超过帖子总数的一半,如果你有一个当前论坛上所有帖子的列表,其中帖子作者的ID也在表中,你能快速找出这个传说中的水王吗?
题意即找出数组中超过一半的数。
最直接的方法是对所有ID排序,然后再扫描一遍排好的ID列表,统计各个ID出现的次数,若某个ID次数超过总数的一半则输出这个ID,这个算法的时间复杂度为O(N * log2N + N)。
但事实上,若ID列表有序,不必再次扫描列表。若一个ID出现的次数超过总数N的一半,那么这个有序的ID列表中的第N/2项(从0开始编号)一定会是这个ID。这样能迅速定位到列表的某一项的话(比如用数组),该时间复杂度为O(N * log2N )。
但上面两种方法都要先对ID列表进行排序,时间复杂度方面没有本质的改进,能否避免排序呢?
如果每次删除 2 个不同的ID(不管是否包含“水王”的ID),那么在剩下的ID列表中,“水王”的ID出现的次数仍然超过总数的一半。可以通过不断重复这个过程把ID列表中的ID总数降低(转化为更小的问题),从而得到答案。
这个思路,避免了排序这个耗时的步骤,总的时间复杂度只有O(N),且只需要常数的额外内存。
1 Type Find(Type *ID, int N){ 2 Type candidate; 3 int nTimes, i; 4 for(i = nTimes = 0; i < N; i++){ 5 if(nTimes == 0){ 6 candidate = ID[i], nTimes = 1; 7 } 8 else{ 9 if(candidate == ID[i]) 10 nTimes++; 11 else 12 nTimes--; 13 } 14 } 15 return candidate; 16 }
这个题目体现了计算机科学中很普通的思想,就是如何把一个问题转化为规模较小的若干个问题。分治、递推和贪心等都是基于这样的思路。在转化的过程中,小的问题跟原问题本质上一致。同样我们可以将小问题转化为更小的问题,因此转化过程是很重要的。像上面这题,我们保证了问题的解在小问题中仍然具有与原问题相同的性质:水王的ID在ID列表中的数量超过一半。转化的效率越高,转化之后问题规模缩小得越快,则整体的时间复杂度越低。
扩展问题:
现在有3个发帖很多的ID,他们的发帖数目都超过了帖子总数目N的1/4,你能从发帖ID列表中快速找出他们的ID吗?
思路与本题一样用抵消法,每次删除 4 个不同的ID即可。
1 Type candidate[3]; 2 void Find1(Type *ID, int N) { 3 int nTimes[3] = {0}; 4 for(int i = 0; i < N; ++i) { 5 if(nTimes[0] == 0) { 6 candidate[0] = ID[i]; 7 nTimes[0] = 1; 8 }else if(nTimes[1] == 0 && ID[i] != candidate[0]) { 9 candidate[1] = ID[i]; 10 nTimes[1] = 1; 11 }else if(nTimes[2] == 0 && ID[i] != candidate[0] && ID[i] != candidate[1]) { 12 candidate[2] = ID[i]; 13 nTimes[2] = 1; 14 }else{ 15 if(candidate[0] == ID[i]) 16 nTimes[0]++; 17 else if(candidate[1] == ID[i]) 18 nTimes[1]++; 19 else if(candidate[2] == ID[i]) 20 nTimes[2]++; 21 else{ 22 nTimes[0]--; 23 nTimes[1]--; 24 nTimes[2]--; 25 } 26 } 27 } 28 }