数据结构-堆

在我看来,js中的堆就是一个比较特殊的数组,我们可以用这个数组中的元素,去生成一个完成的二叉树,同时需要满足,每一个节点的父节点,要大于等于或者小于等于,子节点。

全部大于等于子节点的称之为大顶堆

全部小于等于的为小顶堆

image.png

大顶堆

数组: [10,8,6,7,5,4,3]

每一个父节点都会大于等于其子节点,因此为大顶堆

image.png

小顶堆

数组: [3,5,4,9,7,6,5]

每一个父节点都会小于等于其子节点,因此为小顶堆

image.png

应用

我们可以看一道比较经典的算法题,来理解怎么用js来创建出堆的这种数据结构

image.png

这道题目的解法挺多的,可以用sort去排序,也可以用快排去筛选,我们这里就使用小顶堆。

为什么要使用小根堆而不使用大顶堆,这个我们往下看就知道了。

我们既然要使用堆,自然就需要先创建一个堆了。

创建堆

先创建一个堆的类

class Heap {
    constructor(arr){
        this.heapData = []; //用数组模拟堆
    }
}

新增一个插入heapData的方法,由于要满足每一个父节点都会大于等于其子节点,

因此还要一个交换的方法

function warp(arr,s1,s2){
    //使用结构赋值进行交换
    [arr[s2],arr[s1]] =  [arr[s1],arr[s2]]
}

一个插入的方法

    insert(val){
        this.heapData.push(val);
        let index = this.heapData.length - 1; //获取插入的val在数组中的位置
        while(index){
            //获取index的父节点
            let parent =  Math.floor((index-1)/2);
            //满足小顶堆的条件,父节点小于等于子节点,停止循环
            if(this.heapData[parent] <= this.heapData[index]) {
                break
            }
            //不满足,进行交换
            warp(this.heapData,parent,index);
            //同时,要让父级继续去和上面的元素进行比较
            index = parent;
        }
    }

然后我们在constructor时将传进来的arr,一个个插入,形成一个小顶堆,

创建小顶堆的方法还可以去找到最后一个父节点也就是let i = Math.floor(len/2),然后对比i的子代,如果比它小就进行交换,然后i--,然后继续判断i--的子代,不断重复直到i到达根节点,就结束。

我们继续刚刚的就可以得到如下代码

function warp(arr,s1,s2){
    //使用结构赋值进行交换
    [arr[s2],arr[s1]] =  [arr[s1],arr[s2]]
}

class Heap {
    constructor(arr){
        this.heapData = []; //用数组模拟堆
        //循环插入堆中
        arr.forEach((item)=>{
            this.insert(item);
        })
    }
    insert(val){
        this.heapData.push(val);
        let index = this.heapData.length - 1; //获取插入的val在数组中的位置
        while(index){
            //获取index的父节点
            let parent =  Math.floor((index-1)/2);
            //满足小顶堆的条件,父节点小于等于子节点,停止循环
            if(this.heapData[parent] <= this.heapData[index]) {
                break
            }
            //不满足,进行交换
            warp(this.heapData,parent,index);
            //同时,要让父级继续去和上面的元素进行比较
            index = parent;
        }
    }
}

[3,2,1,5,6,4]为例传入后可以得到[1,3,2,5,6,4]

image.png

由于题目要得是第k大,我们可以从二叉树里面得到的信息有 第一个节点 一定是数组中最小的

因此我们可以截取数组[3,2,1,5,6,4]的前k位,去形成一个大顶堆。以k = 2为例

可以得到 [2,3],2是目前为止最大的,我们继续插入1

由于 1 < 2 ,因为我们要得是第2大的,而1已经比里面的最小的2都要更小了,因此1肯定不是第2大的了

[2,3] 5 由于比2大因此可以替换,

[3,5],然后类推就可以得到[5,6],由此5就是我们要找的第k大了

因此我们可以得到下面的代码

var findKthLargest = function(nums, k) {
    let heap = new Heap(nums.slice(0,k));
    for(let i = k;i<nums.length;i++){
    //不满足条件直接跳过
        if(heap.top()>=nums[i]){
            continue;
        }
        //删除掉堆的根元素,并重新排列
        heap.delChange();
        //插入满足条件的元素
        heap.insert(nums[i])
    }
    return heap.top();
};

delChange如下

delChange(){
    //获取最后一个元素位置
    let len = this.heapData.length - 1;
    let begin = 0;
    //让最后一个元素和第一个进行交换,方便删除
    warp(this.heapData,begin,len);
    //删除最后一个元素
    this.heapData.pop();
    //获取第一个元素的左节点的index
    let changeIndex = 2*begin+1;
    //判断index是否超过了数组长度,实际就是判断存不存左节点
    while(changeIndex < len){
        //获取右节点
        let rightIndex = changeIndex+1;
        //当右节点存在时,对比左右节点的大小,找到小的那个
        if(rightIndex<len && this.heapData[rightIndex]<this.heapData[changeIndex]){
           changeIndex = rightIndex;
        }
        //用左右节点中值小的那个与他们的父节点进行对比,如果比父节点大,终止循环
        if(this.heapData[changeIndex] >= this.heapData[begin]){
            break
         }
         //将父节点与比较小的那个子节点,进行交换
         warp(this.heapData,begin,changeIndex);
         //继续循环判断
         begin = changeIndex;
         changeIndex = 2*begin+1;
   }
}

完整代码如下

function warp(arr,s1,s2){
    //使用结构赋值进行交换
    [arr[s2],arr[s1]] =  [arr[s1],arr[s2]]
}

class Heap {
    constructor(arr){
        this.heapData = []; //用数组模拟堆
        //循环插入堆中
        arr.forEach((item)=>{
            this.insert(item);
        })
    }
    insert(val){
        this.heapData.push(val);
        let index = this.heapData.length - 1; //获取插入的val在数组中的位置
        while(index){
            //获取index的父节点
            let parent =  Math.floor((index-1)/2);
            //满足小顶堆的条件,父节点小于等于子节点,停止循环
            if(this.heapData[parent] <= this.heapData[index]) {
                break
            }
            //不满足,进行交换
            warp(this.heapData,parent,index);
            //同时,要让父级继续去和上面的元素进行比较
            index = parent;
        }
    }
    delChange(){
        //获取最后一个元素位置
        let len = this.heapData.length - 1;
        let begin = 0;
        //让最后一个元素和第一个进行交换,方便删除
        warp(this.heapData,begin,len);
        //删除最后一个元素
        this.heapData.pop();
        //获取第一个元素的左节点的index
        let changeIndex = 2*begin+1;
        //判断index是否超过了数组长度,实际就是判断存不存左节点
        while(changeIndex < len){
            //获取右节点
            let rightIndex = changeIndex+1;
            //当右节点存在时,对比左右节点的大小,找到小的那个
            if(rightIndex<len && this.heapData[rightIndex]<this.heapData[changeIndex]){
               changeIndex = rightIndex;
            }
            //用左右节点中值小的那个与他们的父节点进行对比,如果比父节点大,终止循环
            if(this.heapData[changeIndex] >= this.heapData[begin]){
                break
             }
             //将父节点与比较小的那个子节点,进行交换
             warp(this.heapData,begin,changeIndex);
             //继续循环判断
             begin = changeIndex;
             changeIndex = 2*begin+1;
       }
    }
    top(){
        return this.heapData[0]
    }
}

var findKthLargest = function(nums, k) {
    let heap = new Heap(nums.slice(0,k));
    for(let i = k;i<nums.length;i++){
    //不满足条件直接跳过
        if(heap.top()>=nums[i]){
            continue;
        }
        //删除掉堆的根元素,并重新排列
        heap.delChange();
        //插入满足条件的元素
        heap.insert(nums[i])
    }
    return heap.top();
};