电面的时候问了经典的topK问题,没准备到被问了个措不及防,现在把相关知识点记录下来。
假设我们有一些数据,需要按照某种关键字求出这些数据中最小(大)的K个数,即求出这些数据的topK。
当数据量较小的时候,一个简单的想法是直接对数据进行排序,然后取最前面的K个数据;但是当数据量较大,数据无法一次性放入内存的时候应该怎么办呢?
这时候就需要借助堆这种数据结构。堆通常是一个可以被看做一棵树的数组对象,其总是满足下列性质:
(a)堆中某个节点的值总是不大于或不小于其父节点的值
(b)堆总是一棵完全二叉树
节点值不大于其父节点的堆称为大根堆,反之称为小根堆。
堆排序与topK问题无关,以下内容纯属展开:
以堆为基础的排序方法称为堆排序。堆排序主要可以分为以下几个步骤:
(a)建堆
(b)将堆顶与堆的最后一个元素对换
(c)将堆的大小-1(此时最后一个元素看作已排序好),并对剩下的部分保持堆的性质
(d)重复(b)、(c)直至排序完成
代码如下:
public class MinHeap {
public final static int MAX = 100;
private int[] heap = null;
private int size = 0;
public MinHeap(){
heap = new int[MAX];
}
public boolean add(int num){
if (size >= MAX){
return false;
}
heap[size] = num;
size++;
return true;
}
public void buildHeap(){
for (int i = (size - 1) / 2;i >= 0;i--){
heapify(i);
}
}
private void heapify(int pos){
int left = pos * 2 + 1, right = pos * 2 + 2;
if (left >= size){
return;
}else if (right >= size){
if (heap[pos] > heap[left]){
int tmp = heap[pos];
heap[pos] = heap[left];
heap[left] = tmp;
}
return;
}else{
int minPos = pos;
if (heap[pos] > heap[left]){
if (heap[left] > heap[right]){
minPos = right;
}else{
minPos = left;
}
}else if (heap[pos] > heap[right]){
minPos = right;
}
int tmp = heap[pos];
heap[pos] = heap[minPos];
heap[minPos] = tmp;
if (minPos != pos){
heapify(minPos);
}
}
}
public void heapSort(){
buildHeap();
int length = size;
while (size > 0){
int tmp = heap[size-1];
heap[size-1] = heap[0];
heap[0] = tmp;
size--;
heapify(0);
}
size = length;
}
public void print(){
for (int i = 0;i < size;i++){
System.out.print(heap[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
MinHeap heap = new MinHeap();
for (int i = 10;i > 0;i--){
heap.add(i);
}
heap.heapSort();
heap.print();
}
}
堆排序heapSort()的时间复杂度为O(nlgn),其中建堆buildHeap()的时间复杂度为O(n)(不是笔误,具体见算法导论),每一轮保持堆的性质heapify(int pos)的时间复杂度为O(lgn),一共O(n)轮。
回到topK问题上,我们可以维护一个大小为K的堆来帮助我们解决topK问题。以最小的K个数据为例,我们维护一个大小为K的大根堆在内存中,接下来每次从那一大堆数据当中读出一个数据与堆顶比较,若该数据比堆顶数据小,则说明它比当前topK的最大值要小,因此将堆顶替换为该数据,再用heapify(0)保持该堆的最大堆性质即可。