堆的问题:大堆小堆都能解决LeetCode215 在数组中找第K大的元素

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了堆的问题:大堆小堆都能解决LeetCode215 在数组中找第K大的元素相关的知识,希望对你有一定的参考价值。

LeetCode215题:给定整数数组 nums 和整数 k,请返回数组中第 **k** 个最大的元素。

示例1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

这个题是一道非常重要的题,主要解决方法有三个,选择法,堆排序法和快速排序法。

选择法很简单,就是先遍历一遍找到最大的元素,然后再遍历一遍找第二大的,然后再遍历一遍找第三大的,直到第K次就找到了目标值了。但是这种方法只适合在面试的时候预热,面试官不会让你这么简单就开始写代码,因此该方法的时间复杂度为O(NK)。

这道题比较好的方法是堆和快速排序法。今天我们看堆如何解决问题。首先来看,到底使用哪种堆呢?答案是大堆小堆都可以。

1.基于大顶堆来做

我们先根据之前介绍的建堆方法来建立大顶堆:

 

我们知道每次根节点都是整个堆里最大的那个,那我们每次都删除根节点然后调整,删根节点再调整,反复执行3次,那最后剩下的堆顶是不是就是要找的元素了呢?

具体步骤:

先删除6,再将剩余的元素调整为大顶堆,过程为:

 

然后删除5,再进行第二次调整,依次类推。

执行3轮之后的堆结构如下有图所示。

 这时候直接返回堆的根元素4就行了, 这就是堆是如何解决找第K个问题的。

代码实现比较复杂,我们这里只是看一下就行了,

class Solution {
    public int findKthLargest(int[] nums, int k) {
        int heapSize = nums.length;
        buildMaxHeap(nums, heapSize);
        for (int i = nums.length - 1; i >= nums.length - k + 1; --i) {
            swap(nums, 0, i);
            --heapSize;
            maxHeapify(nums, 0, heapSize);
        }
        return nums[0];
    }

    public void buildMaxHeap(int[] a, int heapSize) {
        for (int i = heapSize / 2; i >= 0; --i) {
            maxHeapify(a, i, heapSize);
        } 
    }

    public void maxHeapify(int[] a, int i, int heapSize) {
        int l = i * 2 + 1, r = i * 2 + 2, largest = i;
        if (l < heapSize && a[l] > a[largest]) {
            largest = l;
        } 
        if (r < heapSize && a[r] > a[largest]) {
            largest = r;
        }
        if (largest != i) {
            swap(a, i, largest);
            maxHeapify(a, largest, heapSize);
        }
    }

    public void swap(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

假如这里nums是一个很大的数据,不是小的数组,或者是流数据, 没头没尾该怎么办呢?堆还能不能解决?可以的,甚至只能用堆来解决。

2.基于小根堆来做

我们推荐使用“找大就用小堆,找小就用大堆”的策略,因为更好理解,适用题目也更多,特别是流数据和大数据的情况。而这正好对应了LeetCode703 数据流中找第K大元素的题目,我们就干脆看一下703题怎么做吧。

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。

请实现 KthLargest 类:

  • KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。

  • int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]

解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3);   // return 4
kthLargest.add(5);   // return 5
kthLargest.add(10);  // return 5
kthLargest.add(9);   // return 8
kthLargest.add(4);   // return 8

这个题说得很啰嗦,无非就是初始堆是4 5 8 2 四个元素,之后不断插入3 5 10 9 4 .....,每次都返回第3大的数字。

这个题几乎只能用堆来进行。同样,我们可以把流数据换成无穷大的数组,大数据等等都是一样的解法。

我们上面介绍的题中,可以先将全部元素塞到完全二叉树里,然后再逐步构造成堆,最后删掉K-1个元素就得到我们需要的了,显然对于流数据和大数据就不行了。

我们需要先构造一个元素大小为K的小根堆,新元素来的时候与根元素对比:

  • 如果新元素比根元素大,则删除根结点,并将新元素入堆。

  • 如果新元素比根结点小,则不做处理。

  • 需要求第K大时,返回根节点就行了。

为啥要这样呢?我们画图看看:

假如原始序列为[5,6,7,8,9,10,11,12,4,3,2,1,......] ,要求第5大的元素。则我们建立一个大小为5的小根堆,然后插入10 的过程为:

 

显然,如果此时要返回第K大自然就是6了。

我们继续插入11 和12之后的结构为:

显然此时的第K大就是8。

如果继续出现 1,2,3,4 等比8小的数,因为都比8小,所以都不会入堆,所以此时第K大还是8。

由此我们看到,不管原始数据是多少,只要用小根堆走一遍就找到需要的结果了。

因此,我们的解题方法是:

我们可以使用一个大小为 k的优先队列来存储前 k 大的元素,其中优先队列的队头为队列中最小的元素,也就是第 k大的元素。

在单次插入的操作中,我们首先将元素val 加入到优先队列中。如果此时优先队列的大小大于 k,我们需要将优先队列的队头元素弹出,以保证优先队列的大小为 k。

代码如下:

class KthLargest {
    PriorityQueue<Integer> pq;
    int k;

    public KthLargest(int k, int[] nums) {
        this.k = k;
        pq = new PriorityQueue<Integer>();
        for (int x : nums) {
            add(x);
        }
    }
    
    public int add(int val) {
        pq.offer(val);
        if (pq.size() > k) {
            pq.poll();
        }
        return pq.peek();
    }
}

 反过来,如果找第K小呢?就要用大根堆了。

所以我们说:找第K大用小根堆,找第K小用大根堆。

以上是关于堆的问题:大堆小堆都能解决LeetCode215 在数组中找第K大的元素的主要内容,如果未能解决你的问题,请参考以下文章

堆的实现及应用

堆(Heap)的性质及实现

算法:正面硬币

堆 (带图详解)

排序4-堆排序与海量TopK问题

堆的常用方法实现