实现JavaScript基本数据结构系列---堆

369 阅读3分钟

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

二叉堆和堆排序

今天我们学一个特殊的二叉树,就是我们的堆数据结构,也叫做二叉堆,他能高效的、快速的找出最大最小值,常被用于优先队列,也用于堆排序算法中。

这一节的内容包括二叉堆数据结构、最大和最小堆、堆排序算法。

二叉堆数据结构

二叉堆是一种特殊的二叉树,有以下两个特性:

  1. 他是一颗完全二叉树,表示树的每一层都有左侧和右侧子节点(除了最后一层的叶子节点),并且最后一层的叶节点尽量是左侧节点,这叫做结构特性
  2. 二叉堆不是最小堆就是最大堆,最小堆允许你快速找到树的最小值,最大堆允许你快速找到树的最大值。所有的节点都大于等于(最大堆)或小于等于(最小堆)每个他的子节点,这叫做堆特性。

实现最小堆

先来定义我们堆的数据结构,他同样需要compareFn函数来比较两个数的值,还需要heap属性来存储我们的堆元素。

const defaultCompare = (a, b) => a > b

class MinHeap {
    constructor(compareFn = defaultCompare) {
        this.heap = [];
        this.compareFn = compareFn;
    }
}

在这里我们用数组来表示二叉堆,通过索引值来检测父节点、左侧和右侧子节点的值,而对于给定index的节点,他的左侧子节点的索引时2 * index + 1,右侧是2 * index + 2,父节点是Math.floor((index - 1) / 2),所以我们可以得到以下函数。

getLeftIndex(index) {
    return 2 * index + 1
}

getRightIndex(index) {
    return 2 * index + 2
}

getParentIndex(index) {
    if (index === 0) {
        return undefined
    }
    return Math.floor((index - 1) / 2)
}

接下来我们来实现最小堆需要的几个基本方法

  • insert(value):像堆中插入一个值,成功返回true,否则返回false
  • extract():移除最小值(最小堆)或最大值(最大堆),并返回该值
  • findMininum():返回最小值(最小堆)或最大值(最大堆),不移除该值

insert

实现步骤:

  1. value 不合法时返回false;合法时插入到数组最后,然后上移
  2. 新插入的值(数组最后一个值)与其父节点对比,要是小于父节点,则二者交换,循环下去,直到大与父节点或者index超出了数组索引。具体实现如下:
insert(value) {
    // value 合法的时候插入,否则插入不成功返回 false
    if (value != null) {
        // 先直接插入到最后,
        this.heap.push(value);
        // 然后进行上移操作9(与父节点比较,小与父节点则交换)
        this.siftUp(this.heap.length - 1)
        return true;
    }
    return false
}

siftUp(index) {
    // 获取父节点,与之比较,小于则交换,直到大于父节点或超出数组为止
    let parent = this.getParentIndex(index);
    // while(index > 0 && this.heap[parent] > this.heap[index]) {
    while (index > 0 && this.compareFn(this.heap[parent], this.heap[index])) {
        [this.heap[parent], this.heap[index]] = [this.heap[index], this.heap[parent]]
        index = parent;
        parent = this.getParentIndex(index);
    }
}

写个例子测一下

const minHeap = new MinHeap()
minHeap.insert(2)
minHeap.insert(3)
minHeap.insert(4)
minHeap.insert(5)

minHeap.insert(1)

console.log(minHeap); // { heap: [ 1, 2, 4, 5, 3 ] }

findMininum

在最小堆中,最小值永远在数组的第一个值,所以函数实现如下:

size() {
    return this.heap.length;
}
isEmpty() {
    return this.size() === 0
}

findMininum() {
    // 或者直接返回 this.heap[0];
    return this.isEmpty() ? undefined : this.heap[0];
}

extract()

extract()函数用来移除最小值(最小堆)或最大值(最大堆),并返回该值。这也是我们接下来要实现的方法。

实现步骤:

  1. 堆为空时返回undefined;只有一个元素时,删除第一个元素并返回
  2. 多个元素时,删除第一个元素并返回,然后下移重新构建最小堆
  3. 删除最小元素后,将第一个元素作为堆顶,对比他的子节点
  4. 分别比较左右节点,找到他比他小的最小的值,然后交换,
  5. 循环第二步
extract() {
    if (this.isEmpty()) return undefined;
    if (this.size() === 1) return this.heap.shift();

    const removeVal = this.heap.shift();
    this.siftDown(0);
    return removeVal;
}

siftDown(index) {
    let element = index;
    const left = this.getLeftIndex(index)
    const right = this.getRightIndex(index);
    const size = this.size()

    if (left < size && this.compareFn(this.heap[element], this.heap[left])) {
        element = left;
    }

    if (right < size && this.compareFn(this.heap[element], this.heap[right])) {
        element = right;
    }

    if (index !== element) {
        [this.heap[index], this.heap[element]] = [this.heap[element], this.heap[index]]
        this.siftDown(element);
    }
}

实现最大堆

最大堆和最小堆的算法一模一样,不同的是要把所有的大于换成小于的比较。

const reverseCompare = compareFn => (a, b) => compareFn(b, a)

class MaxHeap extends MinHeap {
    constructor(compareFn) {
        super(compareFn);
        this.compareFn = reverseCompare(compareFn);
    }
}

测试用例

const maxHeap = new MaxHeap()

maxHeap.insert(2)
maxHeap.insert(3)
maxHeap.insert(4)
maxHeap.insert(5)

maxHeap.insert(1)

console.log(maxHeap); // { heap: [ 5, 4, 3, 2, 1 ]
console.log(maxHeap.findMininum()); // 5