[路飞]_小白学算法之返回第K个XX元素

218 阅读3分钟

哈喽,今天在这里总结一下力扣上遇到的返回第K个最大或者最小元素的问题

首先先看看力扣上的: 215. 数组中的第K个最大元素
返回数组中的第K个最大的元素,那通过按大小排序,返回K-1的下标即可拿到

//暴力解
var findKthLargest = function(nums, k) { 
    nums.sort((a,b)=>b-a) 
    return nums[k-1] 
};

Snipaste_2021-12-19_15-06-24.png 可以看到这里的暴力解是可以通过力扣的测试用例的
那我们再看看另一道题: 703. 数据流中的第 K 大元素
题意大致上就是说每次对数组进行一次添加元素的操作,返回该操作后的第K大的元素,相较于上面一题,多了一步添加的操作
那这里我就可以这样进行暴力解,对每一次添加的时候都进行一次排序,那每次通过访问下标k-1即可得到第K大的元素了

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

很显然当add操作很多时,这种解法的耗时就非常高了

截屏2021-12-19 下午5.38.16.png

这类题,通过了解都可以用到一种叫大(小)顶堆的解决方式

大(小)顶堆是一种经过排序的完全二叉树的形式,那什么是完全二叉树呢
因为完全二叉树的叶子节点的位置比较规律,这里问对二叉树的理解还没有很多,暂时不展开说明,即可理解为如成下图的二叉树,这里是最小堆堆模拟

最小堆初始值.001.jpeg

如果我们进行插入节点(比如插入的是值为5的节点),为了不破坏原先的结构,则进行尾部插入,然后不断向上进行判断是否需要交换节点

最小堆向上迭代.gif

理论上是这样进行的,但是这里会遇到一个困难,就是如果获取节点的父节点呢

子节点父节点的推算.001.jpeg

根据表格中数据可观察出以下规律:
左子节点:leftIndex
右子节点:rightIndex
父节点:parentIndex
leftIndex = (parentIndex + 1) * 2 - 1
rightIndex = leftIndex + 1
parentIndex = leftIndex + 1 >>> 1 或者 ( (leftIndex + 1) /2 需要做小数点都向下 取整)
这里解决了各个节点的获取,那节点的删除与插入是差不多的,插入是向上迭代,那删除就是向下调整,可以取队尾的元素替换到堆顶上进行向下的调整

最小堆向下调整.gif

有了这写前提就可以对703 进行编码

    // 定义类
class MinHeap {
    // 初始化data 是数组
    constructor(data){
        this.data = data || [];
    }

    // 获取data 长度
    size(){
        return this.data.length;
    }

    // 获取最小值,因为是升序长度为k数组所以第一个的元素肯定是第K大的
    peek(){
        return this.size() === 0 ? null : this.data[0];
    }

    //比较元素大小
    compare(a,b){
        return a-b;
    }

    //添加操作
    push(node){
        this.data.push(node);
        //传入最新添加的元素与该元素在数组的下标 进行向上调整
        this.changeUp(node,this.size() - 1);
    }

    //向上调整的方法
    changeUp(node,i){
        let index = i;

        while( index > 0){
            //寻找父节点的下标
            let paretIndex = (index - 1) >>> 1;
            //获取父节点的值
            let parent = this.data[paretIndex];
            //当前节点与父节点比较
            if(this.compare(node,parent) < 0){  
                //当前节点小与父节点,则进行节点的交换
                this.swap(index,paretIndex);
                //交换结束后,由于该节点已经移动到了父节点的位置,所以将父节点的位置继续往上比较调整
                index = paretIndex;
            }else{
                break;
            }
        }
    }

    // 删除元素
    pop(){
        // 如果数组还存在元素
        if(this.size() !== 0){
            //弹出数组最后一个元素
            let last = this.data.pop();
            //将堆顶替换成最后一个元素
            this.data[0] = last;
            //进行向下调整
            this.changeDown(last,0)
        }
        return null
    }

    //向下调整的方法
    changeDown(node,i){
        let index = i;
        let length  = this.size();
        //因为比较的永远是一半的节点,所以只需要需取一半的长度即可
        let halfLen = length >>> 1;
        while(index < halfLen){
            // 获取左子节点,与右子节点的值
            let leftIndex = (index+1) * 2 - 1,left = this.data[leftIndex];
            let rightIndex = leftIndex + 1,right = this.data[rightIndex];

            if(this.compare(left,node) < 0){
                // 如果左子节点的值小于父节点的值 
                if(rightIndex < length && this.compare(right,left) < 0){
                    //左子节点的值大于右节点的值  交换右节点
                    this.swap(rightIndex,index)
                    index = rightIndex
                }else{
                    //左子节点的值小于右节点的值  交换左节点
                    this.swap(leftIndex,index)
                    index = leftIndex
                }
            }else if(rightIndex < length && this.compare(right,node) < 0){
                // 因为上面已经做了左子节点跟父节点的判断,所以右子节点小于父节点的时候 左节点肯定是大于右节点的 可以直接进行交换
                this.swap(rightIndex,index)
                index = rightIndex
            }else{
                // 父节点最小跳出循环
                break;
            }
        }
    }

    //交换元素
    swap(i,j){
        [this.data[i],this.data[j]] = [this.data[j],this.data[i]];
    }
}
var KthLargest = function(k, nums) {
    this.k = k;
    this.heap = new MinHeap()
    //依次添加保证堆的长度与有序性
    for(let node of nums){
        this.add(node)
    }
};

/** 
 * @param {number} val
 * @return {number}
 */
KthLargest.prototype.add = function(val) {
    this.heap.push(val);
    //维护堆的大小一直为k个 超出k个就进行删除操作
    if(this.heap.size() > this.k){
        this.heap.pop()
    }
    return this.heap.peek()
};

这种解法对代码量与复杂度相较于暴力解来说是增加了很多的,但在得到的回馈也是正向增长的

截屏2021-12-19 下午5.38.03.png