[路飞]_leetcode-703-数据流中的第K大元素

147 阅读3分钟

题目描述

[题目地址]

设计一个找到数据流中第 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

提示:

  • 1 <= k <= 104
  • 0 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • -104 <= val <= 104
  • 最多调用 add 方法 104
  • 题目数据保证,在查找第 k 大元素时,数组中至少有 k 个元素

分析: 小顶堆维护K的最大数

这里我们再分享一种在数据结构中常用来维护最值的一种数据结构 =>

堆.png 堆是一种基于完全二叉树的数据结构,它的性质是每一个三元组的最值在根节点 上图是一个大顶堆,所以每一个节点和它的左子树、右子树组成的三元组中,最大的值一定在根节点,又因为整个堆的堆顶元素是所有其余节点的祖先节点,所以整个堆的最值在堆顶元素。

在我们实际的程序中,是使用数组来存储堆这种数据结构的,那么堆中每一个节点对应数组中的小标就是上图用黑色数字标记的值,每一层依次从左到右的放入数组,即为上图右侧的数组。

通过上图我们可以看到每一个节点的左子树的下标等于根节点下标 n*2+1,右子树下标等于根节点下标 n*2+2

根据堆的如上性质,如果我们通过小顶堆,来维护数据流中的前 k 个最大的元素,此时,堆顶的元素,就是数据流中的第 k 大元素。

接下来我们说一下堆的插入操作和弹出操作

堆的插入操作

堆的插入操作对应我们的数组就是在数组末尾插入一个值

此时以途中的大顶堆为例,我们需要判断新插入的值在它对应的三元组中是否大于根节点的值,如果新插入的值大于根节点的值,则和根节点互换位置,不断重复此过程,知道该值小于它的根节点或者该值成为了堆顶元素

如此就完成了堆的插入向上调整

堆的弹出操作

堆的弹出操作指将堆顶元素弹出堆,此时堆顶为空,我们将数组的末尾元素放到堆顶,然后在数组中删除末尾元素

此时之前数组中的末尾元素放在堆顶,肯定是违反了堆的性质的,所以我们要进行向下调整

判断该值是否是当前三元组中的最大值,如果不是,找到当前三元组的最大值,和堆顶元素互换位置,此时堆顶元素来到了第二层,重复以上过程,知道该值是当前三元组的最大值或者该值已经到了我们堆的最底层

如此就完成了堆的弹出向下调整

最后,我们通过手写一个小顶堆,完成本题,代码如下:

/**
 * @param {number} k
 * @param {number[]} nums
 */
var KthLargest = function(k, nums) {
    this.heap = new MinHeap(k);
    for(let i = 0;i<nums.length;i++){
        this.heap.push(nums[i])
    }
};

/** 
 * @param {number} val
 * @return {number}
 */
KthLargest.prototype.add = function(val) {
    this.heap.push(val);
    return this.heap.top();
};

class MinHeap {
    constructor(max){
        this.arr = [];
        this.size = 0;
        this.max = max;
    }
    // 插入
    push(val){
        if(this.size>=this.max&&val<this.top()) return;
        this.arr.push(val);
        this.size++;
        // 插入操作后向上调整
        if(this.size>1){
            let cur =this.size-1;
            let parent = (cur-1) >> 1;
            while(cur>0&&this.arr[parent]>this.arr[cur]){
                [this.arr[parent],this.arr[cur]] = [this.arr[cur],this.arr[parent]]
                cur = parent;
                parent = (cur-1) >> 1;
            }
        }
        // 维护最大个数
        while(this.size>this.max) this.pop();
    }
    // 弹出
    pop(){
        if(this.empty()) return;
        this.arr[0] = this.arr.pop();
        this.size--;
        // 弹出操作后向下调整
        let cur = 0,
        childl = cur*2+1,
        childr = cur*2+2;
        while(
            (childl<this.size&&this.arr[childl]<this.arr[cur])||
            (childr<this.size&&this.arr[childr]<this.arr[cur])
        ){
            if(childr<this.size&&this.arr[childr]<this.arr[childl]){
                [this.arr[cur],this.arr[childr]] = [this.arr[childr],this.arr[cur]]
                cur = childr;
            }else{
                [this.arr[cur],this.arr[childl]] = [this.arr[childl],this.arr[cur]]
                cur = childl;
            }
            childl = cur*2+1;
            childr = cur*2+2;
        }
    }
    // 获取堆顶元素
    top(){
        return this.arr[0]
    }
    // 判空
    empty(){
        return this.size === 0
    }
}

至此,我们通过三种方式完成了leetcode-703-数据流中的第K大元素

如有任何问题或建议,欢迎留言讨论!