JavaScript 实现堆、堆排序、优先队列(附图文)

概述

堆:类似完全二叉树的数据结构。但通常用数组实现。

  • 大顶堆:所有父节点都比子节点大
  • 小顶堆:所有父节点都比子节点小

堆的实现

以小顶堆为例介绍。如下图就是一个小顶堆↓,是类似完全二叉树的数据结构

堆有以下性质

  • 堆维护堆顶永远是当前最值,小顶堆是最小值,大顶堆是最大值
  • 堆只能在一头(堆顶)删除,在另一头(堆底)插入,和队列非常相似

由于堆的这种特性,经常被用来做排序优先队列


下面讲一下堆是如何维护堆顶为当前最值的:

堆顶删除步骤

又称为下沉

  1. 堆顶出堆,将堆底的数据放到堆顶,如图1
  2. 比较左右2个子节点,与更小的节点交换(如果是大顶堆则与更大的交换),如图2
  3. 继续比较左右2个子节点,直到小于等于左右2个子节点或成为叶节点,如图3
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

堆底插入步骤

又称为上浮

  1. 新元素插入到堆底,如图4
  2. 与父节点比较,若该节点小于父节点,则与父节点交换,如图5
  3. 继续和父节点比较,直到大于等于父节点或成为根节点(堆顶)为止,如图6
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

如果再插入一个0,步骤图如下
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述

代码实现

由于堆能维护堆顶为当前最值,因此如果不断取出堆顶元素,就能得到一个单调递增/减的数列,因此堆也经常用来做排序,平均时间复杂度O(NlogN)

虽然前面介绍是用二叉树的形式,但实际中往往使用数组进行堆的构造。因为堆是完全二叉树的形式,而由于完全二叉树的性质,其完全可以使用数组表示,且满足父节点编号=Math.floor(子节点编号/2)(注意不是索引)。上面最后一张图用数组表示就是[0, 1, 2, 4, 5, 6, 3, 7]

构造代码如下:

// 小顶堆
class MinHeap {
    constructor (array = []) {
        this.heapArray = []
        for (let index = 0; index < array.length; index++) {
            this.push(array[index])
        }
    }

    //下沉
    sink () {
        let curIdx = 0
        let len = this.heapArray.length
        let leftChild = 2 * (curIdx + 1) - 1
        let rightChild = leftChild + 1
        let minChild = leftChild
        while (leftChild <= len - 1) { //叶节点跳出循环
            // 获取最小的子节点索引
            // 如果没有右节点,直接选择左节点,否则比较大小
            minChild = rightChild > len - 1 || this.heapArray[leftChild] < this.heapArray[rightChild] ? leftChild : rightChild
            // 交换节点
            if (this.heapArray[curIdx] > this.heapArray[minChild]) {
                [this.heapArray[curIdx], this.heapArray[minChild]] = [this.heapArray[minChild], this.heapArray[curIdx]]
                curIdx = minChild
                leftChild = 2 * (curIdx + 1) - 1
                rightChild = leftChild + 1
            } else {
                return
            }
        }
    }

    //上浮
    swim () {
        let curIdx = this.heapArray.length - 1
        while (curIdx > 0) {
            let fatherIdx = Math.floor((curIdx + 1) / 2) - 1
            if (this.heapArray[curIdx] < this.heapArray[fatherIdx]) {
                [this.heapArray[curIdx], this.heapArray[fatherIdx]] = [this.heapArray[fatherIdx], this.heapArray[curIdx]]
                curIdx = fatherIdx
            } else {
                return
            }
        }
    }

    pop () {
        if (this.heapArray.length === 0) {
            return null
        } else if (this.heapArray.length === 1) {
            return this.heapArray.pop()
        }
        let top = this.heapArray[0]
        this.heapArray[0] = this.heapArray.pop()
        this.sink()
        return top
    }

    push (val) {
        this.heapArray.push(val)
        this.swim()
    }
}

堆排序

前面已经说过,只要不断的将堆顶出列,即可得到单调的序列实现排序。

let heap = new MinHeap([10, 500, 200, 0, 100, 30, 20, 30, 80, 300]);
let sortList = []
while (heap.heapArray.length > 0) {
    sortList.push(heap.pop())
}
console.log(sortList); // [ 0,  10,  20,  30,  30, 80, 100, 200, 300, 500 ]

时间复杂度为O(NlogN),空间复杂度为O(n)


当然也可以原地排序,空间复杂度为O(1),不过这种是大顶堆升序,小顶堆降序

排序逻辑: 先把乱序的数组自底向上调整成堆,再每次循环将堆顶和堆底交换,限制数组长度-1,然后堆顶下沉。代码如下:

这需要稍微改一下sink函数

let sink = (array, idx, len) => {
    // array是待转换的数组,idx是下沉的节点索引,len是以idx为堆顶的数组长度
    let curIdx = idx
    let leftChild = 2 * (curIdx + 1) - 1
    let rightChild = leftChild + 1
    let minChild = leftChild
    while (leftChild <= len - 1) { //叶节点跳出循环
        // 获取最小的子节点索引
        // 如果没有右节点,直接选择左节点,否则比较大小
        minChild = rightChild > len - 1 || array[leftChild] < array[rightChild] ? leftChild : rightChild
        // 交换节点
        if (array[curIdx] > array[minChild]) {
            [array[curIdx], array[minChild]] = [array[minChild], array[curIdx]]
            curIdx = minChild
            leftChild = 2 * (curIdx + 1) - 1
            rightChild = leftChild + 1
        } else {
            return
        }
    }
}

let heapSort = (array) => {
    let len = array.length
    // 调整为小顶堆
    for (let index = Math.floor(len / 2); index >= 0; index--) {
        sink(array, index, len - index)
    }

    // 堆排序
    for (let index = 0; index < len; index++) {
        [array[len - index - 1], array[0]] = [array[0], array[len - index - 1]]
        sink(array, 0, len - index - 1)
    }
}

let array = [10, 500, 200, 0, 100, 30, 20, 30, 80, 300]
heapSort(array)
console.log(array)	// [ 500, 300, 200, 100, 80, 30,  30,  20,  10,  0 ]

优先队列

其实实现了堆,优先队列的逻辑基本上也差不多,只要修改一下权重和优先规则即可。小顶堆就是一个数值越小优先级越大的优先队列

// 优先队列
class PriorityQueue {

    constructor (array = [], cmp = (a, b) => a < b) {
        this.heapArray = []
        this.cmp = cmp // 网上也有 this.cmp = (a, b) => cmp(a, b) ,暂时不知道为什么要做一个函数的拷贝
        for (let index = 0; index < array.length; index++) {
            this.push(array[index])
        }
    }

    //下沉
    sink () {
        let curIdx = 0
        let len = this.heapArray.length
        let leftChild = 2 * (curIdx + 1) - 1
        let rightChild = leftChild + 1
        let minChild = leftChild
        while (leftChild <= len - 1) { //叶节点跳出循环
            minChild = rightChild > len - 1 || this.cmp(this.heapArray[leftChild], this.heapArray[rightChild]) ? leftChild : rightChild
            // 交换节点
            if (this.cmp(this.heapArray[minChild], this.heapArray[curIdx])) {
                [this.heapArray[curIdx], this.heapArray[minChild]] = [this.heapArray[minChild], this.heapArray[curIdx]]
                curIdx = minChild
                leftChild = 2 * (curIdx + 1) - 1
                rightChild = leftChild + 1
            } else {
                return
            }
        }
    }

    //上浮
    swim () {
        let curIdx = this.heapArray.length - 1
        while (curIdx > 0) {
            let fatherIdx = Math.floor((curIdx + 1) / 2) - 1
            if (this.cmp(this.heapArray[curIdx], this.heapArray[fatherIdx])) {
                [this.heapArray[curIdx], this.heapArray[fatherIdx]] = [this.heapArray[fatherIdx], this.heapArray[curIdx]]
                curIdx = fatherIdx
            } else {
                return
            }
        }
    }

    pop () {
        if (this.heapArray.length === 0) {
            return null
        } else if (this.heapArray.length === 1) {
            return this.heapArray.pop()
        }
        let top = this.heapArray[0]
        this.heapArray[0] = this.heapArray.pop()
        this.sink()
        return top
    }

    push (val) {
        this.heapArray.push(val)
        this.swim()
    }
}

let heap = new PriorityQueue([10, 500, 200, 0, 100, 30, 20, 30, 80, 300], (a,b) => a > b); // 试试数值越大优先级越高(大顶堆)
let sortList = []
while (heap.heapArray.length > 0) {
    sortList.push(heap.pop())
}
console.log(sortList); // [ 500, 300, 200, 100, 80, 30,  30,  20,  10,  0 ]

参考:

  1. 优先队列、堆与堆排序详解

  2. [路飞]_优先队列&手撕堆

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值