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

简介: leetcode-703-数据流中的第K大元素


网络异常,图片无法展示
|


「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战


[题目地址][B站地址]


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


题解1. sort排序


本题我们可以想到最简单的办法就是将数组进行从大到小排序,每次 addval 插入 nums 数组,并对数组重新进行排序,返回 nums[k-1] 即可。代码如下:


/**
 * @param {number} k
 * @param {number[]} nums
 */
var KthLargest = function(k, nums) {
    this.k = k;
    this.nums = nums;
    this.nums.sort((a,b) => b-a)
};
/** 
 * @param {number} val
 * @return {number}
 */
KthLargest.prototype.add = function(val) {
    this.nums.push(val);
    this.nums.sort((a,b)=> b-a)
    return this.nums[this.k-1]
};
复制代码


这种方法可以通过本题,但是耗时在 3000ms 左右,是很低效的。


接下来我们对上面代码进行一下优化。


首先,我们根据题意可得我们要找到的是数据流中的第 k 大元素,所以当我们从大到小将数组排序后,数据中第 k 个元素之后的元素对我们是没有意义的,但是这些元素存在在数组中,在我们 add(val) 后再次进行 sort 的时候是很影响效率的,所以我们优化我们的代码如下:


/**
 * @param {number} k
 * @param {number[]} nums
 */
var KthLargest = function(k, nums) {
    this.k = k;
    this.nums = nums;
    this.handle();
};
KthLargest.prototype.handle = function(){
    this.nums.sort((a,b)=> b-a)
    // 维护数组长度
    while(this.nums.length>this.k) this.nums.pop();
}
/** 
 * @param {number} val
 * @return {number}
 */
KthLargest.prototype.add = function(val) {
    this.nums.push(val);
    this.handle();
    return this.nums[this.k-1]
};
复制代码


这一版代码我这里提交耗时 1800ms 左右,可以看到相对于上一版代码是有一个提升的

接下来我们再思考一个问题,当我们要 addval 小于我们当前的 nums[k-1] 的时候,其实我们是不需要将它添加到 nums 数组中的,所以我们优化代码如下:


/**
 * @param {number} k
 * @param {number[]} nums
 */
var KthLargest = function(k, nums) {
    this.k = k;
    this.nums = nums;
    this.handle();
};
KthLargest.prototype.handle = function(val){
// 当数组长度有K个元素且新插入的值小于第K个元素,不进行处理
    if(val!==undefined&&this.nums.length>=this.k&&val<this.nums[this.k-1]) return;
    this.nums.push(val);
    this.nums.sort((a,b)=> b-a)
    // 维护数组长度
    while(this.nums.length>this.k) this.nums.pop();
}
/** 
 * @param {number} val
 * @return {number}
 */
KthLargest.prototype.add = function(val) {
    this.handle(val);
    return this.nums[this.k-1]
};
复制代码


这一版代码我这里提交耗时 1200ms 左右,可以看到相对于上一版代码是有一个提升的。

至此我们通过 sort排序 维护一个从大到小的 k 个最大值的有序数组完成了本题,但是我们优化了两版之后的代码执行耗时还是需要 1200ms 左右,可见这种解题方法不是一个比较高效的解法。


题解2. 二分插入


上面的 sort排序 方法其实很大的开销是在每次插入 val 后都需要对数组重新进行排序,那我们每次将 val 值插入到我们有序数组的合适位置,是不是就可以避免这种每次的排序了呢?


我们再思考一个问题,这里我们要将新的 val 值插入到有序数组的合适位置,这个操作一定是要比 sort排序 的效率要高的,才能保证我们的这种解法比 sort排序 解法更优化,我这里就想到了可以通过 二分查找 来找到将 val 值插入到有序数组的合适位置,如果你对 二分查找 不太熟悉,可以看一下我之前的 《leetcode-35-搜索插入位置》

整理一下二分插入的解题思路:


  1. 初始化的时候对 nums 数组进行排序
  2. add(val) 时通过 二分查找 找到该值在有序数组中的合适插入位置进行插入,并维护 nums 数组的数量
  3. 返回 nums[k-1] 即可


代码如下:


/**
 * @param {number} k
 * @param {number[]} nums
 */
var KthLargest = function(k, nums) {
    this.k = k;
    this.nums = nums;
    this.nums.sort((a,b)=> b-a)
};
KthLargest.prototype.handle = function(val){
    // 当数组长度有K个元素且新插入的值小于第K个元素,不进行处理
    if(val!==undefined&&this.nums.length>=this.k&&val<this.nums[this.k-1]) return;
    // 特判数组长度为1的情况
    if(this.nums.length === 1){
        if(this.nums[0]>val) this.nums.push(val)
        else this.nums.unshift(val)
    }else{
        // 二分查找合适的插入位置
        let l = 0,r = this.nums.length-1;
        while(l<r){
            const mid = (l+r) >> 1;
            if(this.nums[mid]>val) l = mid+1;
            else r = mid;
        }
        this.nums.splice(l,0,val)
    }
    // 维护数组长度
    while(this.nums.length>this.k) this.nums.pop();
}
/** 
 * @param {number} val
 * @return {number}
 */
KthLargest.prototype.add = function(val) {
    this.handle(val);
    return this.nums[this.k-1]
};
复制代码


这一版代码我这里提交耗时 130ms 左右,基本达到了一个最优解的耗时


题解3. 小顶堆维护K的最大数


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


网络异常,图片无法展示
|


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


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


通过上图我们可以看到每一个节点的左子树的下标等于根节点下标 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
    }
}
复制代码


这一版代码我这里提交耗时 130ms 左右,同样达到了一个最优解的耗时


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


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


相关文章
|
2月前
【力扣】-- 移除链表元素
【力扣】-- 移除链表元素
38 1
|
2月前
【LeetCode 27】347.前k个高频元素
【LeetCode 27】347.前k个高频元素
38 0
|
2月前
【LeetCode 06】203.移除链表元素
【LeetCode 06】203.移除链表元素
33 0
|
2月前
【LeetCode-每日一题】移除元素
【LeetCode-每日一题】移除元素
33 0
|
4月前
|
存储 算法
LeetCode第83题删除排序链表中的重复元素
文章介绍了LeetCode第83题"删除排序链表中的重复元素"的解法,使用双指针技术在原链表上原地删除重复元素,提供了一种时间和空间效率都较高的解决方案。
LeetCode第83题删除排序链表中的重复元素
|
4月前
|
算法 索引
LeetCode第34题在排序数组中查找元素的第一个和最后一个位置
这篇文章介绍了LeetCode第34题"在排序数组中查找元素的第一个和最后一个位置"的解题方法,通过使用双指针法从数组两端向中间同时查找目标值,有效地找到了目标值的首次和最后一次出现的索引位置。
LeetCode第34题在排序数组中查找元素的第一个和最后一个位置
|
4月前
|
Python
【Leetcode刷题Python】剑指 Offer II 082. 含有重复元素集合的组合
解决LeetCode平台《剑指 Offer II 082. 含有重复元素集合的组合》题目的Python代码实现,通过深度优先搜索算法找出所有和为特定目标值的数字组合,并在搜索过程中通过排序和跳过重复元素来避免解集中出现重复组合。
43 2
|
4月前
|
算法
LeetCode第27题移除元素
这篇文章介绍了LeetCode第27题"移除元素"的解题方法,通过使用双指针技巧,有效移除数组中特定值的元素并返回新数组的长度。
|
4月前
|
算法 索引 Python
【Leetcode刷题Python】34. 在排序数组中查找元素的第一个和最后一个位置(二分查找)
解决LeetCode "在排序数组中查找元素的第一个和最后一个位置" 问题的方法。第一种方法是使用两次二分查找,首先找到目标值的最左边界,然后找到最右边界。第二种方法是利用Python的list.index()方法,先正序找到起始位置,再逆序找到结束位置,并给出了两种方法的Python实现代码。
69 0
|
4月前
|
Python
【Leetcode刷题Python】203.移除链表元素
文章提供了三种删除链表中特定值节点的方法:迭代法、虚拟节点迭代删除法和递归法,并给出了相应的Python实现代码。
28 0