这个问题来自于寒枫天伤的一个post:一个网友的面试题。这里假设M是一个相当大的数,N是相对小很多的常数。例如,在几百万个已知数中求10个最大的数。在寒枫天伤的entry后面,不少人都通过comment给出了自己的想法,我也曾被问过这个题目,在这里说说我的思路。
我基本上比较同意fan1的说法:
设置10个变量,最开始取头10个数字将10个变量填满,并进行排序,然后对一百万个数字进行一次遍历,每一个数字先比对10个变量的最小值,如果结果为小,则进行下一个数字,反之则淘汰当前最小变量,然后将当前数字插入适当位置,之后继续下一个数字比较。
后面,尉迟方做了这样的反驳:
呵呵fans1的算法应该不是最优的吧,只看到他一个一个的往里塞 怎么塞进去的却不管了 最坏情况是每个都要插入&移动10次,那就是10 × n。加上查找位置的比较时间,二分好了,算最坏的,4次,就是4 × n
我认为,这个插入和查找的过程是可以统一起来的,查找的问题是不存在的。当然最坏情况是要做10次的比较和移动,但是因为N相对于M是一个很小的数字,所以,算法复杂度仍然是会在O(n)规模上。
还有一位叫Xiaochengyong的朋友提出这样的方法:
使用类似Radix Sorting 的方法。就像从一个班的考试试卷找出前10名一样。 一、第一次遍历,从1到n,将试卷按照0-10分、10-20、20-30、...、80-90、90-100分进行分别堆放。 二、考察90-100中的元素数目, 如果大于10,则只考虑90-100分的区段,这样就丢掉了0-90分的试卷;则如果小于10,例如只有3份,再考察80-90分区段依次类推。 继续使用该算法,对90-100分的区段进行分类,例如91-92、...,99-100,依次类推... 就本题目来说,一百万个元素(假设每个元素在0-100之间),在第一次从1到n的遍历后,就丢掉了0-90的大量元素,依次下去,每次都可以丢掉局部元素中的较大部分元素。这种算法的复杂度大概应该是T(n)=n+n/10+n/(10*10)+...n/(10*...10)。
我认为,首先一点这M个数分布区域可能非常大,不能假设它们处于1-100之间,其次,按照基数排序的方式,是否需要辅助存储空间,这个空间复杂度似乎作者也没有考虑。
所以,我当时的答案也和fan1一样。并且,在排序的临时存储空间边界上设定一个“哨兵”,可以减少比较的次数,对于如此大规模的运算过程,效率上的提高是非常显著的。按照这个思路,写了下面的这段小程序,其中data[]为数据样本,top[]用于存放最大的N个数的辅助空间:
int data[M];
int top[N + 1];
void top_N()
{
top[0] = std::numeric_limits<int>::max(); // Set a "guard" on the boundary to reduce comparision times.
for (int i = 1; i <= N; ++i) top[i] = std::numeric_limits<int>::min();
for (int j = 0; j < M; ++j)
{
for (int k = N; top[k] < data[j]; --k) top[k] = top[k - 1];
top[k + 1] = data[j];
}
}
我不知道是否还有更优的做法了,我能想到的也就是这个办法了。