这是编程之美书第2.5节的一道题目。
解法二,用选择排序或冒泡排序,复杂度O(NK)。但这方法也做了不必要做的一件事:对想得到的K个数进行了排序。方法不可取。
解法三,用顺序统计位(类快排)算法来计算(可参考算法导论)。算法导论上说这种方法从平均性能上来讲是线性的,但编程之美上却说复杂度是O(N*lgK)。对于这点,我对编程之美持怀疑态度。我认为,对于一个无序的数组,用顺序统计位算法,可以在近似O(n)的时间复杂度内得到第K大的元素。当这个过程执行完毕时,数组中位于该元素之前的元素就是前K大的元素。
解法三,先查找出数组中的Vmax和Vmin,组成新的数组[Vmin,Vmax],二分查找该数组,并在原数组中查找。具体请参见编程之美。复杂度为O(nlgn),方法不可取。
解法四,维护一个K大小的数组A,遍历原数组中的每个数,与A每个元素进行比较,如果大于A数组中的最小元素,则交换,否则继续。复杂度为O(NK),方法不可取。
解法五,维护一个最小堆,大小为K。一开始先从原数组中随便取K个元素建最小堆,复杂度为O(K)。然后遍历原数组中剩余的n-K个元素,每个元素先与堆顶比较,如果大于堆顶则交换,并维护最小堆性质。总复杂度为K+(n-K)lgK = nlgK。该方法只需要遍历一次数组,且无须在内存中存储所有数组数据,而只需维护K大小的数据。是一种适用于 大数据,小内存的好方法。如果K还是太大,则分次来求,通过先用K'(K'<K)来求……具体请参见编程之美。[1]
解法六,利用hash保存数组中元素Si出现的次数,利用计数排序的思想,线性从大到小扫描过程中,前面有k-1个数则为第k大数,平均情况下时间复杂度O(n)。这个方法要求数组中的取值范围跨度不能太大。
解法七,对于原数组A,它的取值范围跨度为[Vmin,Vmax],直接用hash来统计的方法就是解法六。这里,我们把该范围平均分成M份,每份范围跨度都是N/M。遍历原数组,分别统计各个跨度中每个元素出现的次数。从[Vmax-N/M, Vmax] 遍历到 [Vmin, Vmin+N/M],累加元素出现的次数。当发现某个跨度累加超过K时,那么我们就知道第K大元素在这个跨度里。这样,我们就可以在该跨度里用Hash的方法寻找该元素(该跨度很小,可以在内存中存放Hash表)。编程之美中的时间复杂度有待研究。
解法八,有文章表明,用“败者树”有可能比堆得到更好的结果。参见[6]。是一个北交大的学生写的。
网上[2]有关该题的简短解答。他认为上面的各种解法都可以应用在本题上。但我认为并不可以这样来做。因为本题是想找“不同”的前K大数。用堆排序,中位数方法无法计算重复的数据。所以,个人认为可以用两种方法: A. 完全排序。然后顺序找前K个“不同”的元素。 B. 用桶排序的方法。重复的数据被映射到桶中,只算一个。 还要注意的是,浮点数的相同应该用fabsf(a-b)<0.00001等类似的方法来判断。
同[2]的中解答。
用映射二分堆的方式。用O(4n)的方法对原数组建最大堆,然后pop出k次即可。时间复杂度为O(4n + k*logn)。映射二分堆与普通堆不同的地方是:它的节点并不真正保存数据单元本身,而是保存指向数据单元的指针。因此 当需要交换父子节点的数据时,可以避免拷贝大量数据所消耗的时间。同时,映射二分堆还有一个功能可以根据具体的数据单元的索引来删除该单元,即使这个单元 不是堆中的最值。[4].
4.在实际应用中,还有一个“精确度”的问题。我们可能并不需要返回严格意义上的最大的K个元素,在边界位置允许出现一些误差。当用户输入一个query的时候,对于每一个文档d来说,它跟这个query之间都有一个相关性衡量权重f (query, d)。搜索引擎需要返回给用户的就是相关性权重最大的K个 网页。如果每页10个网页,用户不会关心第1000页开外搜索结果的“精确度”,稍有误差是可以接受的。比如我们可以返回相关性第10 001大的网页,而不是第9999大的。在这种情况下,算法该如何改进才能更快更有效率呢?网页的数目可能大到一台机器无法容纳得下,这时怎么办呢?
参考[2]. 正如提示中所说,可以让每台机器返回最相关的K'个文档,然后利用归并排序的思想,得到所有文档中最相关的K个。 最好的情况是这K个文档在所有机器中平均分布,这时每台机器只要K' = K / n (n为所有机器总数);最坏情况,所有最相关的K个文档只出现在其中的某一台机器上,这时K'需近似等于K了。我觉得比较好的做法可以在每台机器上维护一 个堆,然后对堆顶元素实行归并排序。个人觉得这还是正确的解答。没有回答90%准确时的高效率做法。如果是用维护堆的做法,这是完全精确的。
参考[2]. 肯定是有帮助的。在搜索关键字qj最相关的K个文档时,可以在qj的“近义词”相关文档中搜索部分,然后在全局的所有文档中在搜索部分。