0. 问题介绍
top k就是求解一个数字队列前k大的问题,在工作后者面试中是一个非常常见的问题,这里说明两种解法。
1.基于快排的解法
1.1 算法思路
这里假设你对快排已经熟悉。我们知道快排是随机找个标识,然后用此标识进行排序。
我们进行降序排序的方式,第一次进行排序后,就能获得在序列中的大小位置。如果它正好是第k大,那么它左边的数组就是题目要求的top k(当然,这是很幸运的情况,我们要讨论算法的一般性)。
如果返回的标示所在数组的位置(下文称为index)大于k,那topk都在左边,但是也包含了不属于topk的数组,还需要对其进行排序。
如果index小于k,那左边都是topk的成员,但是还不够,需要在右边获取剩下(k-index)个。
就这么一直找下去,直到index等于k
1.2 代码实现
private void topk(int[] data, int low, int high,int k) {
int start = low;
int end = high;
int mid = data[low];
while (low < high) {
while (low < high && data[high] <= mid) {
high--;
}
if (low < high) {
data[low] = data[high];
low++;
}
while (low < high && data[low] >= mid) {
low++;
}
if (low < high) {
data[high] = data[low];
high--;
}
}
data[low] = mid;
if(low>k){
topk(data,start,low-1,k);
}else if(low<k){
topk(data,low+1,end,k);
}
}
@Test
public void test() {
int[] array = {1, 10, 2, 20, 3, 30, 4, 40, 5, 50, 6, 60, 7, 70, 8, 80, 9, 90};
int k = 10;
topk(array, k);
for (int i = 0; i < k; i++) {
System.out.println(array[i]);
}
}
1.3 复杂度证明
这个算法的复杂度是O(n)级别的
可以看到,我们是基于二分来做的,但是我们每次排序只是需要排序的地方,并不是对全部数据进行排序。
第一次全部比较,一共比较了n次,经过二分后,我们对剩下的部分进行比较也就是n/2,接下来n/4,n/8,n/16 … 1 一共需要比较logn次。
根据等比数列求和 可以算出来为2n次。所以在复杂度级别上是O(n)。
2.使用堆排
2.1 算法思路
维护一个容量为k的堆保存topk的数组,然后对每个数字比较一遍。思路也很简单,复杂度是O(nlogk)
2.2 代码实现
java中直接使用TreeSet(红黑树实现的)来实现堆的作用
private TreeSet<Integer> topk(int[] array, int n) {
TreeSet<Integer> set = new TreeSet<Integer>();
for (int i = 0; i < array.length; i++) {
int value = array[i];
if (set.size() < n) set.add(value);
else {
int min = set.first();
if (value > min) {
set.remove(min);
set.add(value);
}
}
}
return set;
}
最后总结一下,虽然基于堆的实现在复杂度和额外空间开销上都差于基于二分的算法。但如果你要排序的数组是特别大,不太容易直接加载到内存上,第二种算法倒是最合适的。