二叉堆
二叉堆是一种特殊的二叉树。
它的特性:
- 是一棵完全二叉树。树的每一层都有左侧和右侧子节点,除了最后一层的叶节点。最后一层的叶准备节点尽可能都是左侧子节点,这是结构特性。
- 二叉堆不是最小堆就是最大堆。最小堆可以快速导出数的最小值,最大堆可以快速导出数的最大值。所有的节点都大于等于(最大堆)或小于等于(最小堆)每个它的子节点。这是堆特性。
图示:
创建最小堆
首先我们来创建一个类:
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1,
EQUALS: 0,
};
function defaultCompare(a, b) {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
class MinHeap {
constructor(compareFn = defaultCompare) {
/**比较函数 */
this.compareFn = compareFn;
/**存储数据 */
this.heap = [];
}
}
二叉树的数组表示
二叉树的两种表示方式:
- 用指针表示。
- 使用一个数组通过索引值检索父节点、左侧和右侧子节点的值。
使用index访问数组表示的二叉树节点。
对于给定位置index的节点:
- 它的左侧子节点的位置是:2 * index + 1(如果位置可用);
- 它的右侧子节点的位置是:2 * index + 2(如果位置可用);
- 它的父侧子节点的位置是:index / 2(如果位置可用);
在类中添加访问节特定节点方法:
/**
* 获取当前位置的左节点位置
* @param {*} index
* @returns
*/
getLeftIndex(index) {
return 2 * index + 1;
}
/**
* 获取当前位置的右节点位置
* @param {*} index
* @returns
*/
getRightIndex(index) {
return 2 * index + 2;
}
/**
* 获取当前位置的右节点位置
* @param {*} index
* @returns
*/
getParentIndex(index) {
if (index === 0) return undefined;
return Math.floor((index - 1) / 2);
}
向堆中插入值
向堆中插入值是指将值插入堆的底部叶节点(数组的最后一个位置)。再执行siftUp方法,表示我们将要将这个值和它的父节点进行交换,直到父节点小于这个插入的值。
向堆中插入新值的方法实现:
/**
* 向堆中插入一个值
* @param {*} value
*/
insert(value) {
if (value != null) {
this.heap.push(value);
this.siftUp(this.heap.length - 1);
return true;
}
return false;
}
siftUp方法实现:
/**
* 将这个值和它的父节点进行交换,直到父节点小于这个插入的值
* @param {number} index 索引
*/
siftUp(index) {
// 获取其父节点的位置
let parent = this.getParentIndex(index);
// 如果插入的值小于它的父节点(在最小堆中,或在最大堆中比父节点大),那么我们将这个元素和父节点交换。
while (
index > 0 &&
this.compareFn(this.heap[parent], this.heap[index]) > Compare.EQUALS
) {
swap(this.heap, parent, index);
index = parent;
parent = this.getParentIndex(index);
}
}
siftUp方法接收插入值的位置作为参数。我们同样需要获取其父节点的位置。
如果插入的值小于它的父节点(在最小堆中,或在最大堆中比父节点大),那么我们将这个元素和父节点交换。我们重复这个过程,直到堆的根节点也经过了交换节点和父节点位置的操作。
交换方法如下:
/**
* 交换数组中两个位置的值
* @param {array} array
* @param {number} a
* @param {number} b
*/
function swap(array, a, b) {
return ([array[a], array[b]] = [array[b], array[a]]);
}
插入节点示例:
const heap = new MinHeap();
heap.insert(2);
heap.insert(3);
heap.insert(4);
heap.insert(5);
heap.insert(1);
示意图:
从堆中找到最大值或最小值
在最小堆中,最小值总是位于数组的第一个位置(堆的根节点)。
代码如下:
/**堆的大小 */
size() {
return this.heap.length;
}
/**是否为空 */
isEmpty() {
return this.size() === 0;
}
/**获取堆中的最小值。 */
findMinimum() {
// 如果堆不为空,我们返回数组的第一个值。
return this.isEmpty() ? undefined : this.heap[0];
}
获取堆中的最小值。
const heap = new MinHeap();
heap.insert(2);
heap.insert(3);
heap.insert(4);
heap.insert(5);
heap.insert(1);
console.log(heap.findMinimum());// =>1
注意: 在最大堆中,数组的第一个元素保存了最大值,所以我们可以使用相同的代码。
导出堆中的最大值或最小值
移除最小值(最小堆)或最大值(最大堆)表示移除数组中的第一个元素(堆的根节点)。
移除后将堆的最后一个元素移动至根部,并执行siftDown函数表示我们将交换元素,直到堆的结构正常。
代码如下:
/**移除最小值(最小堆)或最大值(最大堆) */
extract() {
if (this.isEmpty()) return undefined;
if (this.size() === 1) return this.heap.shift();
const removedValue = this.heap.shift();
this.heap.unshift(this.heap.pop())
this.siftDown(0);
return removedValue;
}
/**
* 交换元素,直到堆的结构正常
* @param {number} index 移除元素的位置
*/
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]) > Compare.EQUALS
) {
element = left;
}
if (
right < size &&
this.compareFn(this.heap[element], this.heap[right]) >
Compare.EQUALS
) {
element = right;
}
if (index !== element) {
swap(this.heap, index, element);
this.siftDown(element)
}
}
示例:
const heap = new MinHeap();
heap.insert(1);
heap.insert(2);
heap.insert(3);
heap.insert(4);
heap.insert(5);
console.log(heap.heap);// [1, 2, 3, 4, 5]
console.log(heap.extract());
console.log(heap.heap);// [2, 4, 3, 5]
创建最大堆
最大堆的算法和最小堆的算法一模一样,不同之处在于我们要把所有>的比较换成<的比较。
代码如下:
function reverserCompare(compareFn) {
return (a, b) => compareFn(b, a);
}
class MaxHeap extends MinHeap {
constructor(compareFn = defaultCompare) {
super(compareFn);
this.compareFn = reverserCompare(compareFn);
}
}
const heap = new MaxHeap();
heap.insert(2)
heap.insert(3)
heap.insert(4)
heap.insert(5)
heap.insert(1)
console.log(heap);
console.log(heap.findMinimum());
最大堆的最大值是堆的根节点。
堆排序算法
堆排序算法实现步骤:
- 使用数组创建一个最大堆用作数据源。
- 创建最大堆后,最大的值会被存储在堆的第一个位置。接着将它替换为堆的最后一个值,将堆的大小减1。
- 最后将堆的根节点下移,并重复步骤2,直到堆的大小为1。
可用最大堆得到一个升序排列的数组(从最小到最大)。若想数组按降序排列,可用最小堆代替。
堆排序算法代码:
function heapify(array, index, heapSize, compareFn) {
let largest = index;
const left = (2 * index) + 1;
const right = (2 * index) + 2;
if (left < heapSize && compareFn(array[left], array[index]) > 0) {
largest = left;
}
if (right < heapSize && compareFn(array[right], array[largest]) > 0) {
largest = right;
}
if (largest !== index) {
swap(array, index, largest);
heapify(array, largest, heapSize, compareFn);
}
}
function buildMaxHeap(array, compareFn) {
for (let i = Math.floor(array.length / 2); i >= 0; i -= 1) {
heapify(array, i, array.length, compareFn);
}
return array;
}
function heapSort(array, compareFn = defaultCompare) {
let heapSize = array.length;
buildMaxHeap(array, compareFn);
while (heapSize > 1) {
swap(array, 0, --heapSize);
heapify(array, 0, heapSize, compareFn);
}
return array;
}