TopK引入
又这么几种问题:求出当前数组最小值、求出当前数据流中第K大的数。
前者是一个固定的范围,而后者是一个变化的范围(当然了,你也可以看作若干个固定范围的快照)。解决以上问题的一个思路是排序,则难以避免需要扫描整个数组的值,而基于堆排序的时间复杂度是O(n*logK)
调整的时间复杂度取决于堆的高度,将K个元素组成一个堆(数组形式的完全二叉树),因此可以调整的时间复杂度就是logK,如果你对整个数组建堆那么调整的时间复杂度就是logN
基于堆去做topK,是一个排除法的思路。
以求当前数组最小值为例,我先拿前K个元素建立一个大顶堆,那么堆顶元素必然是当前最大元素,那么当我遇到第K+1个元素,如果K+1比堆顶元素还大则直接忽略,如果第K+1个元素小于堆顶元素,我就把堆顶元素poll()移除,然后再做一个调整,最终堆顶将又是一个最大元素(比刚才移除的小)…最终遍历完整个数组,我们没做一次调整,就是就是将一个局部最大值移除的过程,有些更大的值甚至不进入offer()。相当于对一个数组移除了n-k个局部最大值,那么剩余的便是K个最小值,而最终堆首便是第K小的值。
总结:堆中构造TopK最大的本质是排除数组中Top(n-k)最小,因此我们需要一个能够快速拿到当前堆最小值的调用,并且排除这个最小的值后调整。
一句话:求Top大用小顶堆,求Top小用大顶堆。
public int[] getLeastNumbers(int[] arr, int k) {
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> b - a);
for(int i:arr){
if(pq.size()<k){
//大小为K的堆还没有建成
pq.offer(i);
}else {
if(!pq.isEmpty()&&pq.peek()>i){
//排除一个局部最大值,加入一个新元素并调整
pq.poll();
pq.offer(i);
}
}
}
int[] res = new int[k];
int index=0;
while(k-->0){
res[index++]=pq.poll();
}
return res;
}
java的priorityQueue底层就是一个堆(默认是小顶堆,可以通过构造函数传入比较器对象调整为大顶堆),其中offer()和poll()都涉及堆调整操作。
补充:大数据下的TopK
如果数据多到无法直接载入内存,可以将数据对应的大文件拆分成若干个小文件(至少这些文件对应的数据能够一次载入内存),然后依次读入这些文件,并且在内存中只维护一个大小为K的堆/优先队列,最终这些数据全部被扫描,而且堆/优先队列中保存就是K个数字,堆顶就是对应的Topk
堆排序
堆排序的过程也是一个排除的过程——假如我们想要对大小为N的数组进行排序,那么我们便可以先对这个数组建立一个大小为N的堆,这个堆其实是一个数组,但是堆建立完毕不代表数组已经有序了,堆建立完毕只是表示:当前堆的堆顶一定是一个最值。
而排序的过程就是把这个最值往目标数组移动,然后调整堆的过程。假如我排序升序,我们我可以使用一个大顶堆,每次把堆顶拿走,然后放入到当前数组的末尾,并且对前N-1个堆进行调整。最终使得整个数组都是升序排列的。(注意,因为是对数组整体建堆,因此每次拿到的堆顶一定是全局最大的、全局第二大、全局第三大…而前面TopK仅对K个元素建堆)
堆排序时一般都是升序——大顶堆,堆顶元素会和末尾元素(这里的末尾是依次收缩的)交换,因此原地交换使得空间复杂度为O(1),你也可以使用一个第三方数组、然后小顶堆,每次拿到最小的往第三方数组中放,但是这样空间复杂度就上升到O(n)了。这里主要指小顶堆也