一、背景
实际应用中,我们经常面临这样的问题,即从一个序列S中选择其中最大的K个数(一般情况下K远小于|S|),我们将这种问题称为TopK问题。
举一个例子,美剧《权利的游戏》中的每个人物,观众都会对其进行选择支持或不支持,这样每个人物都会都应一个热度值,这个值可以是支持者的数量,我们可以发现在腾讯视频中该剧中热度排名前3的人物加以了高亮标识,如下图:
诸如此类的应用场景很多,本文借助快速排序的思想,给出解决此问题的一个时间复杂度为O(N)的算法。
二、快速排序思想
为了引出本文基于快排TopK的思想,先给出快排的算法描述,令S为待排序集合,|S|表示集合元素数量。
1、如果 S 中元素个数是0或1,则返回。
2、取 S 中任一元素 v, 称之为枢纽元。
3、将 S - {v} (S中其余元素) 分成两个不相交的集合:
和
。
4、返回 {
后,继随
, 继而
}。
本人博客https://blog.csdn.net/gaoxueyi551/article/details/46778181中的代码便是该思想的实现,可供参考。
三、基于快速排序的TopK算法
其实,只要稍加修改快排算法,即可实现平均时间复杂度为O(N)的TopK算法,我们称之为QuickSelect(S, K)。
1、如果|S| = 1,返回S,否则若|S| < 20,则使用选择排序对S排序,选择最大的K个元素返回
2、选取一个枢纽元 v 属于 S。(同快速排序步骤2)
3、将集合 S - {v} 分割成 S1 和 S2。 (同快速排序步骤3)
4、如果K <= |S1| ,则K个元素必然全部位于集合S1中,并返回QuickSelect(S1, K);如果K = |S1| + 1,则集合S1 与 枢纽元 v 恰好是所求的K个数,我们将 S1 和 v 一并返回;如果K > |S1| + 1,那么S1和枢纽元v必然是K个元素的一部分,剩余K - |S1| - 1 个元素必然存在集合 S2 中,因此我们应该返回 S1 + v + QuickSelect(S2, K - |S1| - 1)。
仔细分析该算法可得如下结论:
- 快速选择算法的递归调用次数是快速排序的一半。
- 两种算法的最坏情形都是O(N*N),即集合已排序的情况。
- 算法平均时间复杂度为O(N)。
结论2易于理解。分析算法可知,每一轮递归开始之前,我们已经消去了1个集合,递归仅作用在另外一个集合,结论2于是成立。结论3的推导依赖结论1。
在理想情况下,枢纽元 v 的选择使每轮递归都近似的将当前集合等分,故最多需要递归O(logN)次:
第1次递归对应的集合长度为 |S / 2|,
第2次递归对应的集合长度为 |S / 4|,
第3次递归对应的集合长度为 |S / 8|,
......
第log(N) 次递归对应的集合长度为 1,
将O(logN)次递归的集合长度相加,
Sum = |S / 2| + |S / 4| + |S / 8| + ...... + 1
这是等比数列,根据等比数列求和方法可得Sum大约等于2N,故快速选择的平均时间复杂度为O(N)。