堆、堆排序的javascript基础篇

724 阅读4分钟

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

前面我们讲了树,今天我们讲一种特殊的树,堆(Heap)

ps:看没看完麻烦顺手点个赞~

堆的定义

  • 堆是一种完全二叉树 -- 完全二叉树是除了最下层节点不满外,其他节点必须是满节点,且最下层所有节点靠左
  • 堆的当前节点必须大于等于(小于等于)它的子节点 -- 当前节点大于等于子节点的堆叫做大顶堆,小于等于子节点的堆叫做小顶堆

image.png

堆的实现

因为堆是一个完全二叉树,我们可以方便的使用数组存储堆(数组从1位开始,是为了方便计数)

image.png

如何往堆中插入元素?

例如往上面的大顶堆中插入数字5,我们自下而上比较节点,不满足大顶堆的规则就交换,满足就跳出循环

image.png

const Heap = function() {
    // 当前堆
    this.heap = [null, 6, 5, 4, 3, 2, 1];
    // 当前堆内数据量
    this.count = 6;
    this.add(5);
}

Heap.prototype.add = function(val) {
    this.heap.push(val);
    this.count++;
    heapifyUp(this.heap, this.count);
}

// 自下往上堆化
const heapifyUp = function(heap, i) {
    while (i >> 1 > 0 && heap[i >> 1] < heap[i]) {
        swap(heap, i >> 1, i);
        i = i >> 1;
    }
}

const swap = function(arr, a, b) {
    [arr[a], arr[b]] = [arr[b], arr[a]];
}

如何从堆顶删除数据

将堆顶数据删除,并将堆底数据放到堆顶,然后对堆进行自上而下的堆化。 将当前节点与左节点进行比较,选出最小值的下标,再与右节点比较,选出最小值下标,然后交换。若没有节点交换,跳出循环,否则将指针指向刚刚的最小节点,继续上述操作。

image.png

const Heap = function() {
    this.heap = [null, 6, 5, 4, 3, 2, 1];
    this.count = 6;
}

Heap.prototype.removeTop = function() {
    // 交换堆顶和堆底
    swap(this.heap, 1, this.count);
    const topVal = this.heap.pop();
    this.count--;
    heapifyDown(this.heap, 1, this.count);
    return topVal;
}

const heapifyDown = function(heap, i, n) {
    while(true) {
        let posi = i;
        if (i * 2 <= n && heap[i * 2] > heap[i]) posi = i * 2;
        if (i * 2 + 1 <= n && heap[i * 2 + 1] > heap[posi]) posi = i * 2 + 1;
        if (i === posi) break;
        swap(heap, i, posi);
        i = posi;
    }
}

const swap = function(arr, a, b) {
    [arr[a], arr[b]] = [arr[b], arr[a]];
}

const heap = new Heap();
heap.removeTop(); // 6
heap.removeTop(); // 5
...

如何对数组进行堆化排序

假设有一个无序数组[2,3,6,4,5,1],需要进行升序排列,首先需要先建堆,然后对他进行排序

建堆方案有两种:

  • 一种是类似新增数据的方案,自下而上从数组尾部进行堆化,因为堆化一个节点的时间复杂度为O(logn),顶部节点不需要排序,所以这种建堆的时间复杂度为(n-1) * O(logn) = O(nlogn)

  • 还有一种方案是自上而下的方案,因为自上而下,底部所有叶子节点不需要堆化,从最后一个非叶子节点往下堆化,所以时间复杂度是不是为(n/2) * O(logn) = O(nlogn)?其实不是,因为子节点的堆化程度跟高度成正比,顶部的堆化程度最高,底部最低。 所以时间复杂度其实为O(n)

下面我们根据第二种建堆方案来排序:

// 建堆
const buildHeap = function(nums){
    nums.unshift(null);
    const n = nums.length;
    for (let i = n >> 1; i > 0; i--) {
        heapify(nums, n, i)
    }
    return nums;
}

// 堆化
const heapify = function(heap, n, i) {
    while(true) {
        let maxPos = i;
        if (i * 2 <= n && heap[i * 2] > heap[i]) maxPos = i * 2;
        if (i * 2 + 1 <= n && heap[i * 2 + 1] > heap[maxPos]) maxPos = i * 2 + 1;
        if (maxPos === i) break;
        swap(heap, maxPos, i);
        i = maxPos;
    }
}

// 排序
const sort = function(nums) {
    let n = nums.length;
    let heap = buildHeap(nums);
    while (len > 1) {
        swap(heap, 1, n);
        n--;
        heapify(heap, n, 1)
    }
    return heap.shift();
}


const swap = function(arr, a, b) {
    [arr[a], arr[b]] = [arr[b], arr[a]];
}

排序时不需要额外创建空间,因为我们生成的是大顶堆,堆顶是最大值,将他与数组最末尾交换,然后堆化前n-1的堆,再将堆化后的堆顶与倒数第二位交换,如此即排序成功。

排序的时间复杂度是O(nlogn),所以总的排序方法的时间复杂度是O(n) * O(nlogn)=O(nlogn)

堆化排序是原地排序算法,空间复杂度是O(1),因为排序会交换位置,所以是非稳定排序算法。

建堆整体代码:

class Heap {
    // type = greater, less
    constructor(type, max, compare) {
        this.heap = [];
        this.type = type || 'less';
        this.count = 0;
        this.max = max || Infinity;
        this.compare = compare || null;
    }
}

Heap.prototype.push = function (val) {
    if (this.count === this.max) return false;
    this.heap[this.count++] = val;
    this.heapifyUp();
    return true;
}

Heap.prototype.pop = function () {
    if (this.count === 0) return false;
    const top = this.heap[0];
    this.swap(0, this.count - 1);
    this.count--;
    this.heapifyDown();
    return top;
}

Heap.prototype.heapifyDown = function () {
    const compare = this.compare;
    let i = 0;
    while (i < this.count) {
        let temp = i;
        // 小顶堆
        if (this.type === 'less') {
            if (i * 2 + 1 < this.count && (compare ? compare(this.heap[i], this.heap[i * 2 + 1]) : this.heap[i] > this.heap[i * 2 + 1])) {
                temp = i * 2 + 1;
            }
            if (i * 2 + 2 < this.count && (compare ? compare(this.heap[temp], this.heap[i * 2 + 2]) : this.heap[temp] > this.heap[i * 2 + 2])) {
                temp = i * 2 + 2;
            }

        } else {
            if (i * 2 + 1 < this.count && (compare ? compare(this.heap[i], this.heap[i * 2 + 1]) : this.heap[i] < this.heap[i * 2 + 1])) {
                temp = i * 2 + 1;
            }
            if (i * 2 + 2 < this.count && (compare ? compare(this.heap[temp], this.heap[i * 2 + 2]) : this.heap[temp] < this.heap[i * 2 + 2])) {
                temp = i * 2 + 2;
            }
        }
        if (temp === i) break;
        this.swap(temp, i);
        i = temp;
    }
}

Heap.prototype.heapifyUp = function () {
    const compare = this.compare;
    let i = this.count - 1;
    while (i > 0) {
        if (this.type === 'less') {
            if (compare ? compare(this.heap[i], this.heap[(i - 1) >> 1]) : this.heap[i] < this.heap[(i - 1) >> 1]) {
                this.swap(i, (i - 1) >> 1)
                i = (i - 1) >> 1
            } else {
                break;
            }
        } else {
            if (compare ? compare(this.heap[i], this.heap[(i - 1) >> 1]) : this.heap[i] > this.heap[(i - 1) >> 1]) {
                this.swap(i, (i - 1) >> 1)
                i = (i - 1) >> 1
            } else {
                break;
            }
        }
    }
}

Heap.prototype.swap = function (a, b) {
    [this.heap[a], this.heap[b]] = [this.heap[b], this.heap[a]]
}

Heap.prototype.get = function () {
    return this.heap;
}

Heap.prototype.getTop = function () {
    return this.heap[0];
}

Heap.prototype.getSize = function () {
    return this.count;
}