一起学习数据结构与算法:堆

152 阅读3分钟

// 堆的底层实际上是一棵完全二叉树。
// 每个的节点元素值不小于其子节点 - 最大堆 type = max
// 每个的节点元素值不大于其子节点 - 最小堆 type = min
// 可以用数组实现
// 父节点 i 的左子节点在位置 2 * i + 1
// 父节点 i 的右子节点在位置 2 * i + 2
// 子节点 i 的父节点在位置 Math.floor((i -1) / 2)
class Heap {
    constructor(values = [], type = 'max') {
        [this.data, this.type] = [values, type]
        Object.defineProperties(this, {
            // 长度
            length: {
                configurable: false,
                enumerable: true,
                get() {
                    return this.data && this.data.length || 0
                },
                set() {
                    return console.warn(`property length is readonly`);
                }
            }
        })
        if(this.length) this.create()
    }
    create() {
        // 得到第一个非叶子节点
        const notLeafNode = Math.floor(this.data.length / 2) - 1
        // 以这个节点为起点开始调整节点位置
        for(let i = notLeafNode; i >=0; i--) {
            this.adjust(i)
        }
    }
    // 调整节点的位置
    // 公式: 第i个节点的左子节点:2*1+1;第i个节点的右子节点:2*1+2;
    adjust(curIndex, data = this.data, length = this.length) {
        // 从当前节点(curIndex)开始遍历,
        // 左子节点(i = 2 * curIndex + 1),每次遍历完成,当前节点置换为左子节点(i = 2 * i + 1),临界条件是当前遍历的节点是叶子节点了(i < length)
        for(let i = 2 * curIndex + 1; i < length; i = 2 * i + 1) {
            // i 为左节点, i+1为右节点,如果右节点存在的话,要比较左节点和右节点的大小
            // 如果是max堆,且右节点大于左节点,把i设设置成右节点的索引,即i++
            // 如果是min堆,且右节点小于左节点,把i设设置成右节点的索引,即i++
            if(i + 1 < length) {
                if((this.type === 'max' && data[i + 1] > data[i]) || (this.type === 'min' && data[i + 1] < data[i])) i++
            }
            // 如果是max堆,且当前节点小于子节点中的最大值,交换两个值的位置,curIndex置换为子节点中最大值的索引
            // 如果是min堆,且当前节点大于子节点中的最大值,交换两个值的位置,curIndex置换为子节点中最小值的索引
            if((this.type === 'max' && data[curIndex] < data[i])
            || (this.type === 'min' && data[curIndex] > data[i])) {
                [data[curIndex], data[i]] = [data[i], data[curIndex]]
                curIndex = i
            } else {
                break
            }
        }
    }
    // 由于堆属于优先队列,只能从末尾添加
    // 添加后有可能破坏堆的结构,需要从下到上进行调整
    push(value) {
        this.data.push(value);
        if (this.length > 1) {
            // 从最后一个节点开始上遍历
            let index = this.length - 1;
            // target为父节点
            let target = Math.floor((index - 1) / 2);
            while (target >= 0) {
                if ((this.type === 'min' && this.data[index] < this.data[target]) ||
                    (this.type === 'max' && this.data[index] > this.data[target])) {
                    [this.data[index], this.data[target]] = [this.data[target], this.data[index]]
                    index = target;
                    target = Math.floor((index - 1) / 2)
                } else {
                    break
                }
            }
        }
    }
    // 由于堆属于优先队列,只能从头部移除
    // 移除头部后,使用末尾元素填充头部,从头部开始调整节点位置
    pop() {
        if(!this.length) return null
        if(this.length === 1) return this.data.pop()
        const result = this.data[0]
        this.data[0] = this.data.pop()
        this.adjust(0)
        return result
    }
    // 堆排序
    // max 从大到小,先构建最小堆,然后循环:交换顶部和尾部,从新排序
    // min 从小到大,先构建最大堆,然后循环:交换顶部和尾部,从新排序
    // 复杂度分析:adjust函数中相当于堆的每一层只遍历一个结点,因为具有n个结点的完全二叉树的深度为[log2n]+1,
    // 所以adjust的复杂度为 O(logn),而外层循环共有 f(n) 次,所以最终的复杂度为 O(nlogn)。
    static sort(data = [], type = 'max') {
        let result = new Heap(data, type === 'max' ? 'min' : 'max')
        // 如果是最小堆,根 节点总是最小的,如果是最大堆,根节点总是最大的。
        // 根节点与最后一个节点交换
        // 从根节点开始调整,并且最后一个结点已经为当
        // 前最大值,不需要再参与比较,所以第三个参数
        // 为 i,即比较到最后一个结点前一个即可
        for(let i = result.data.length - 1; i >= 0; i--) {
            [result.data[0], result.data[i]] = [result.data[i], result.data[0]]
            result.adjust(0, result.data, i)
        }
        return result.data
    }
}

const heap = new Heap([5,6,7,8,1,2,3,4,9])
console.log('最大堆数据:', heap.data)
console.log('堆顶数据出队列:', heap.pop())
console.log('移除堆顶后的数据-最大堆:', heap.data)
heap.push(9)
console.log('9入堆后的数据-最大堆:', heap.data)
console.log('堆排序-从小到大:', Heap.sort([5,6,7,28,1,22,34,4,9], 'min'))
console.log('堆排序-从大到小:', Heap.sort([5,6,7,28,1,22,34,4,9], 'max'))