优先队列--堆

138 阅读3分钟

废话不多说,先上图。如图,是一个堆的基本结构,也是一棵完全二叉树。 image.png

和普通的二叉树相比,堆满足一个性质。

大顶堆任意节点的子节点不大于它本身, 因此第一层,堆元素就是整个堆的最大值, 也称最大堆。

小顶堆任意节点的子节点不小于它本身, 因此第一层,堆元素就是整个堆的最小值, 也称最小堆

作为一个数据结构, 构建出来之后,还要维护它,保持它的性质。

堆又称优先队列, 因为实际上在操作一个堆的时候,我们是按队列来操作的, 而维护它的时候,把他当做一棵树。而,完全二叉树可以根据索引找到对应的父子节点, 因此一个数组就可以建堆。

下面按最小堆,来说明堆的出入操作以及维护。

出堆 和出队操作一样,剔除堆顶元素shift。不同的是,后续的维护, 要让堆尾元素补到堆顶的位置,然后向下和子节点比较,和最小的节点进行交换。交换后,继续与新的子节点进行比较交换,直到子节点都不小于其本身,或者抵达最后一层。

入堆 和入队操作一样, 从堆尾入堆push。然后,和其父节点相比较,如果父节点大于它,就交换。然后,继续与新的父节点进行比较交换,直到父节点都不大于其本身,或者抵达堆顶。

下面这种实现,是简单的维护了堆的性质,限制了堆的大小,但不保证是最小或最大的那一部分。

基础版堆

class Heap {
    /* 这里当然是数组建堆, 作为完全二叉树 可以利用索引找到每个节点的父节点和子节点
        索引为 n 它的父节点的索引就是(n-1)>>1    子节点的索引就是 2n+1 2n +2
        compare 函数决定这是个最大堆 还是最小堆
    */
    constructor(arr, compare = (a, b) => a - b > 0, size = 63) {
        // 初始数据建堆 是从顶部开始的, 虽然这是个金字塔 ,但是地基我们最后打
        this.data = [...arr]; // 深拷贝一下
        this.maxLen = size;
        this.compare = compare
        for (let i = 1; i < this.size; i++) {
            this.bubleUp(i)
        }

        // 这里可以用数学归纳法来验证, 向一个已经建好的堆添加新的元素 ,肯定是放在最下面,然后逐个,冒泡调整上去,这样维持了堆的性质,它仍然是一个堆,
        // 这里是先建堆的第一层 只有一个元素 ,肯定是个堆, 然后第二层依次与第一个元素冒泡,这样一个三元最小堆就完成, 下面的就一样了
        if (this.size > size) {
            /* 这种写法是直接截取金字塔的最上面几层, 没有排序达不到求最小最大k个数 */
            this.data.length = size
        }
    }
    get size() { return this.data.length }


    bubleUp(i) {
        if (i == 0) return
        // let pInd = (i- 1) >>1
        // if (this.compare(this.data[pInd], this.data[i]) ){
        //     this.swap(pInd, i)
        // }
        // this.bubleUp(pInd);
        // 迭代
        while (i) {
            let pInd = (i - 1) >> 1
            if (this.compare(this.data[pInd], this.data[i])) {
                this.swap(pInd, i)
                i = pInd
            } else {
                break
            }
        }
    }
    bubleDown(i) {
        const [l, r] = [2 * i + 1, 2 * i + 2]
        if (l >= this.size) {
            return
        }
        // 这里就是要保证i这个位置的值是三者最小 不过 右孩子不一定存在 , 先记录索引比较完成之后只交换一次即可
        /*         let min  = i
                if (this.compare(this.data[i] , this.data[l])){
                    min = l
                }
                if (r < this.size && this.compare(this.data[i] , this.data[r])){
                    min = r
                }
                if (min !== i){
                    this.swap(min,i)
                    this.bubleDown(min)
                } */

        //迭代
        let cur = i
        while (2 * cur + 1 < this.size) {
            const [l, r] = [2 * cur + 1, 2 * cur + 2]
            let min = cur
            if (l<this.size && this.compare(this.data[i], this.data[l])) {
                min = l
            }
            if (r < this.size && this.compare(this.data[i], this.data[r])) {
                min = r
            }
            if (min !== i) {
                this.swap(min, i)
                cur = min;
            } else {
                break
            }
        }

    }

    swap(a, b) {

        [this.data[a], this.data[b]] = [this.data[b], this.data[a]]
    }
    // 堆又称又称优先队列 就是因为 堆都是从底部进去 从顶部出去 就像 做蛋糕的挤奶油那样
    // 这里限制一下堆的大小就和队列差不多了  
    push(el) {
  
        this.data.push(el)
        this.bubleUp(this.size - 1)
        /* 先进来再出队 这样就不用外部判断 */
        if (this.size > this.maxLen) {
            this.pop()
        }
    }
    //  堆排序就是通过不断pop出堆的最值堆顶元素来实现的 ,pop完了之后 维持住堆的性质
    //  pop了 之后 顶部空缺 尾部来补 这样数据的变动最小
    pop() {
        if (this.size === 0) return

        const ret = this.data.shift()
       
        this.data.unshift(this.data.pop())
        this.bubleDown(0)

        return ret
    }
    get top() { return this.data[0] } // 堆顶元素就是0


}
function getRandArr(n) {
    const arr = []
    for (let i = 0; i < n; i++) {
        arr.push(Math.random() * 90 + 10 | 0);
    }
    return arr

}

复杂度分析

尝试分析一波复杂度。

先假设,我们的堆不限空间,两种建堆方式都需要遍历整个数组,并且遍历的过程中,对其进行向下向上的冒泡维护操作。这个冒泡操作,最多也就是log2(n)-1),所以维护操作的时间复杂度是O(logn)的再乘上遍历。建堆的时间复杂度就是O(n* logN). 后续的每次维护操作是(logn)。

因此如果用堆排序, 先建堆然后不断的出堆,那么时间复杂度就是(n* logN) + (n* logN),也就是 O((n* logN)).

适用场景

求最值。 用堆求最值不需要全排序, 尤其是后续数据变动,维护起来方便。

刷题版堆

而这种实现就保证了,一定是一堆数据中最小或最大的那一部分。

class Heap2 {
    /* 默认最小堆 */
    constructor(data = [], k = 0, compare = (a, b) => a > b) {
        this.data = [];
        this.limit = k;
        this.compare = compare
        for (let i = 0; i < data.length; i++) {
            this.push(data[i])
        }
    }
    /* 建堆从最高层开始 ,从堆尾入堆, */
    bubleUp(index) {
        /* 父节点的索引为 n/2 -1 */
        let pInd = (index - 1) / 2 | 0;
        while (index) {
            if (this.comp(pInd, index)) {
                this.swap(pInd, index)
                index = pInd;
                pInd = (index - 1) / 2 | 0
            } else {
                break
            }
        }

    }
    bubleDown(index) {
        /*  先比较然后直接与最小的进行交换*/
        let left = index * 2 + 1, right = index * 2 + 2, min = index;
        while (left < this.data.length || right < this.data.length) {
            if (left < this.data.length  && this.comp(min, left)) {
                min = left
            }
            if (right < this.data.length  && this.comp(min, right)) {
                min = right
            }
            if (min !== index) {
                this.swap(min, index)
                index = min;
                left = index * 2 + 1;
                right = index * 2 + 2;
                min = index;
            } else {
                break
            }

        }

    }
    comp(a, b) {
        return this.compare(this.data[a], this.data[b])
    }
    swap(a, b) {
        let t = this.data[a];
        this.data[a] = this.data[b]
        this.data[b] = t
    }
    push(v) {

        this.data.push(v);
        this.bubleUp(this.data.length - 1)


        if (this.data.length > this.limit) {
            this.pop()

        }

    }
    pop() {
        /* 优先队列 队首出队 队尾补到队首 */
        let last = this.data.pop()
        const res = this.data[0];
        this.data[0] = last
        this.bubleDown(0)
        return res

    }
    get top() { return this.data[0] }
}

打印完全二叉树

顺便附上,打印完全二叉树的方法,便于观察。下面是两位数为基准的,所以空格也是两个空格。如果数据的位数,不固定无法保证打印结果的直观性。

// 输出二叉树 默认数组是层序遍历完全二叉树
function logBinaryTree(arr = []) {
    let len = arr.length;
    // 计算深度从0开始  高度为depth+ 1
    let depth = 0, yushu = len
    while (yushu >= 1 << depth) {
        yushu = yushu - (1 << depth);
        depth++
    }
    let maxLen = 1 << depth;
    console.log('层数', depth);
    for (let index = 0; index <= depth; index++) {
        const l = (1 << index) - 1, r = (1 << index) + l;
        // 补位是也是翻倍 2^n -1  间距是翻倍的
        let diff = depth - index + 1, gap = (1 << diff) - 1, pre = (1 << (diff - 1)) - 1
        // console.log('间距', gap, '补位', pre, l, r);
        console.log()
        let temp = arr.slice(l, r)
        if (index) {

            console.log('  '.repeat(pre) + temp.join('  '.repeat(gap)))
        } else {
            console.log('  '.repeat(pre) + temp.join(''))
        }

    }
}