海量数据TopK问题


TopK问题:在大量的数据中找出前k个最大值或者最小值

对于海量数据,如果我们只需要找出其中最大的k个或者最小的k个,怎样才能找出呢?

假设我们对所有的数据进行排序,时间复杂度(快速排序):O(nlog2n); 如果这是数亿个数据,那这样的方法也就不那么高效了

哪哈有没有更加高效的方法呢?

利用堆来解决TopK问题(以最大的k个举例)

我们知道大根堆的堆顶是堆中的最大元素;而小根堆的堆顶是堆中的最小元素;

思路是:我们可以建立一个大小为K小根堆,然后从第k个元素开始遍历,如果大于小根堆的堆顶,那么就交换两个元素,交换一次调整一次,直到所有数据遍历结束,堆中的元素就是海量数据中最大的k个

为什么是小根堆,而不是大根堆呢?大根堆的堆顶是堆中的最大值呀

答:我们的目的是小根堆中保存所有数据中最大的k个,正因为小根堆的堆顶是最小的值,当我们遍历剩余的数据时,只有大于我们这个堆中的最小值,才有资格放进来,这样当遍历结束后堆中的元素就是最大的k个;

为什么建立大小为k呢?

答:因为我们只是想要找出海量数据中最大或最小的k个,所以只需要大小为k;

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    /**
     * 海量数据中最大的k个
     * @param arr
     * @param k
     * @return
     */
    public static int[] smallKHeap(int[] arr,int k) {
        int[] kHeap = new int[k];
        for (int i = 0; i < k; i++) {
            kHeap[i] = arr[i];
        }

        //建立大小为k的小堆
        for (int i = (k-1-1)/2; i >= 0; i--) {
            adjustSmallDown(kHeap,i,k-1);
        }

        /**
         * 从下标k开始和堆顶比较,大于堆顶元素交换并且调整
         */
        for (int i = k; i < arr.length; i++) {
            if (arr[i] > kHeap[0]){
                kHeap[0] = arr[i];
                adjustSmallDown(kHeap,0,k-1);
            }
        }
        return kHeap;
    }

    //调整
    public static void adjustSmallDown(int[] arr,int i,int end) {
        int parent = i;
        int child = 2*parent + 1;
        while(child <= end) {
            if (child+1 <= end && arr[child+1] < arr[child]) {
                child = child+1;
            }

            //child 下标保存的是最小的值
            if (arr[child] < arr[parent]) {
                int tmp = arr[child];
                arr[child] = arr[parent];
                arr[parent] = tmp;
                parent = child;
                child = 2*parent + 1;
            }else {
                break;
            }
        }
    }

相同的原理求最小的k个就不再累述,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    /**
     * 海量数据中最小的k个
     * @param arr
     * @param k
     */
    public static int[] largeKHeap(int[] arr,int k) {
        int[] kHeap = new int[k];
        for (int i = 0; i < k; i++) {
            kHeap[i] = arr[i];
        }
        /**
         * 建大小为k的大堆 -》 求最小得k个数
         */
        for (int i = (k-1-1)/2; i >= 0; i--) {
            adjustLargeDown(kHeap,i,k-1);
        }
        /**
         * 从下标为k依次开始和堆顶元素比较,小于堆顶元素 交换 重新调整
         */
        for (int i = k; i < arr.length; i++) {
            if (arr[i] < kHeap[0]) {
                kHeap[0] = arr[i];
                adjustLargeDown(kHeap,0,k-1);
            }
        }
        return kHeap;
    }

    //调整    
    public static void adjustLargeDown(int[] arr,int i,int end) {
        int parent = i;
        int child = parent*2+1;
        while (child <= end) {
            if (child+1 <= end && arr[child+1] > arr[child]) {
                child = child+1;
            }
            //child 保存的是孩子中最大值得下标
            if (arr[child] > arr[parent]) {
                int tmp = arr[child];
                arr[child] = arr[parent];
                arr[parent] = tmp;
                parent = child;
                child = parent*2+1;
            }else {
                break;
            }
        }
    }

性能分析

时间复杂度:O(n*log2k)

空间复杂度:O(k)