今天我们将要学习一种特殊的二叉树,也就是堆数据结构,也叫作二叉堆。二叉堆是计算机科学中一种非常著名的数据结构,由于它能高效、快速地找出最大值和最小值,常被应用于优先队列。它也被用于著名的堆排序算法中。
二叉堆的特性
二叉堆是一种特殊的二叉树,有以下两个特性:
- 结构特性:它是一棵完全二叉树,表示树的每一层都有左右子节点(除了最后一层的叶节点),并且最后一层的叶节点尽可能都是左侧子节点(先左子树再右子树,左子树不完整则不能向右子树插入节点)。
- 堆特性:二叉堆不是最小堆就是最大堆。最小堆允许你快速导出树的最小值,最大堆允许你快速导出树的最大值。所有的节点都都大于等于(最大堆)或小于等于(最小堆)每个它的子节点。
二叉堆的实现
1. 创建最小二叉堆类
二叉树有两种表示方式。第一种是使用一个链表。第二种是使用一个数组,通过索引值检索父节点、左侧和右侧子节点的值。
今天我们采用数组来实现二叉堆。首先我们创建一个类,代码如下:
class MinHeap<T = unknown> {
private heap: T[];
private compare: (a: T, b: T) => boolean;
constructor(defaultCompare?: (a: T, b: T) => boolean) {
this.heap = [];
this.compare = defaultCompare || MinHeap.compareFn;
}
/**
* 默认的比较节点大小函数,a大于b返回true
* @param a
* @param b
* @returns
*/
private static compareFn(a: any, b: any) {
return a > b;
}
}
2. 添加访问特定节点的方法
因为我们是采用数组方式表示二叉堆,所以我们需要实现一些特定方法来访问其自身的父、子节点。
如上图的最小二叉堆,用数组表示为[1, 2, 3, 4, 5, 6, 7].
对于给定位置 index 的节点:
- 它的左侧子节点的位置是 2 * index + 1(如果位置可用);
- 它的右侧子节点的位置是 2 * index + 2(如果位置可用);
- 它的父节点的位置是 (index - 1) / 2 取整(如果位置可用)。
代码实现如下:
/**
* 获取节点的左侧子节点的位置
* @param idx 节点位置
* @returns
*/
private getLeftIdx(idx: number) {
return idx * 2 + 1;
}
/**
* 获取节点的右侧子节点的位置
* @param idx 节点位置
* @returns
*/
private getRightIdx(idx: number) {
return idx * 2 + 2;
}
/**
* 根据子节点的位置得到父节点的位置
* @param idx 子节点位置
* @returns
*/
private getParentNodeIdx(idx: number) {
if (idx === 0) {
return undefined;
}
return Math.floor((idx - 1) / 2);
}
3. 向最小二叉堆中插入值
将值插入堆的底部叶节点,代码如下:
insert(value: T) {
if (this.isEmpty()) {
this.heap.push(value);
return;
}
this.heap.push(value);
this.siftUp(this.heap.length - 1);
}
将值插入堆后,我们还需要考虑堆是否合法。如果不合法,则需要通过siftUp方法将堆合法化,如图所示:
代码实现如下:
/**
* 上移操作:插入节点后保证最小堆的特性(所有的节点都小于等于它的子节点)
* @param idx 插入节点的位置
*/
private siftUp(idx: number) {
let tempIdx = idx;
let parentIdx = this.getParentNodeIdx(tempIdx);
// 如果插入的值小于其父节点的值,则交换它们的位置,直到其大于等于父节点的值
while(parentIdx !== undefined && this.compare(this.heap[parentIdx], this.heap[tempIdx])) {
[this.heap[tempIdx], this.heap[parentIdx]] = [this.heap[parentIdx], this.heap[tempIdx]];
tempIdx = parentIdx;
parentIdx = this.getParentNodeIdx(tempIdx);
}
}
4. 导出最小二叉堆的最小值
移除最小值表示移除数组的第一个元素(堆的根节点)。在移除后,我们将堆的最后一个元素移动至根部并执行 siftDown 函数,表示我们将交换元素直到堆的结构正常。如图所示:
代码实现如下:
/**
* 下移操作:移除最小值后,保证二叉堆的特性
*/
private siftDown(idx: number = 0): void {
// 先找到左右子节点哪个更小,再交换位置,重复此操作
let minIdx = idx;
const leftIdx = this.getLeftIdx(idx);
const rightIdx = this.getRightIdx(idx);
if (leftIdx < this.heap.length && this.compare(this.heap[minIdx], this.heap[leftIdx])) {
minIdx = leftIdx;
}
if (rightIdx < this.heap.length && this.compare(this.heap[minIdx], this.heap[rightIdx])) {
minIdx = rightIdx;
}
if (minIdx === idx) {
return;
}
[this.heap[idx], this.heap[minIdx]] = [this.heap[minIdx], this.heap[idx]];
return this.siftDown(minIdx);
}
/**
* 移除最小值并返回
*/
extract() {
if (this.heap.length === 0) {
return undefined;
}
if (this.heap.length <= 2) {
return this.heap.shift();
}
const backVal = this.heap.shift();
this.heap.unshift(this.heap.pop() as T);
this.siftDown();
return backVal;
}
至此我们实现了一个简单的最小二叉堆结构。
二叉堆实现完整代码
最小二叉堆完整实现代码如下:
class MinHeap<T = unknown> {
private heap: T[];
private compare: (a: T, b: T) => boolean;
constructor(defaultCompare?: (a: T, b: T) => boolean) {
this.heap = [];
this.compare = defaultCompare || MinHeap.compareFn;
}
/**
* 默认的比较节点大小函数,a大于b返回true
* @param a
* @param b
* @returns
*/
private static compareFn(a: any, b: any) {
return a > b;
}
/**
* 获取节点的左侧子节点的位置
* @param idx 节点位置
* @returns
*/
private getLeftIdx(idx: number) {
return idx * 2 + 1;
}
/**
* 获取节点的右侧子节点的位置
* @param idx 节点位置
* @returns
*/
private getRightIdx(idx: number) {
return idx * 2 + 2;
}
/**
* 根据子节点的位置得到父节点的位置
* @param idx 子节点位置
* @returns
*/
private getParentNodeIdx(idx: number) {
if (idx === 0) {
return undefined;
}
return Math.floor((idx - 1) / 2);
}
/**
* 上移操作:插入节点后保证最小堆的特性(所有的节点都小于等于它的子节点)
* @param idx 插入节点的位置
*/
private siftUp(idx: number) {
let tempIdx = idx;
let parentIdx = this.getParentNodeIdx(tempIdx);
// 如果插入的值小于其父节点的值,则交换它们的位置,直到其大于等于父节点的值
while(parentIdx !== undefined && this.compare(this.heap[parentIdx], this.heap[tempIdx])) {
[this.heap[tempIdx], this.heap[parentIdx]] = [this.heap[parentIdx], this.heap[tempIdx]];
tempIdx = parentIdx;
parentIdx = this.getParentNodeIdx(tempIdx);
}
}
/**
* 下移操作:移除最小值后,保证二叉堆的特性
*/
private siftDown(idx: number = 0): void {
// 先找到左右子节点哪个更小,再交换位置,重复此操作
let minIdx = idx;
const leftIdx = this.getLeftIdx(idx);
const rightIdx = this.getRightIdx(idx);
if (leftIdx < this.heap.length && this.compare(this.heap[minIdx], this.heap[leftIdx])) {
minIdx = leftIdx;
}
if (rightIdx < this.heap.length && this.compare(this.heap[minIdx], this.heap[rightIdx])) {
minIdx = rightIdx;
}
if (minIdx === idx) {
return;
}
[this.heap[idx], this.heap[minIdx]] = [this.heap[minIdx], this.heap[idx]];
return this.siftDown(minIdx);
}
/**
* 查找最小值并返回
* @returns
*/
findMinimum() {
return this.heap[0];
}
size() {
return this.heap.length;
}
isEmpty() {
return this.size() === 0;
}
insert(value: T) {
if (this.isEmpty()) {
this.heap.push(value);
return true;
}
this.heap.push(value);
this.siftUp(this.heap.length - 1);
return true;
}
/**
* 移除最小值并返回
*/
extract() {
if (this.heap.length === 0) {
return undefined;
}
if (this.heap.length <= 2) {
return this.heap.shift();
}
const backVal = this.heap.shift();
this.heap.unshift(this.heap.pop() as T);
this.siftDown();
return backVal;
}
toString(nodeToString: (key: T) => string = (key) => `${key}`) {
if (this.heap.length === 0) {
return '';
}
const twoArr: string[][] = [];
this.heap.forEach((value, idx) => {
const str = nodeToString(value);
if (idx === 0) {
twoArr.push([str]);
return;
}
const lastIdx = twoArr.length - 1;
if (twoArr[lastIdx].length === 2 ** lastIdx){
twoArr.push([]);
}
twoArr[twoArr.length - 1].push(str);
});
return twoArr.map((val, idx) => {
return `第${idx + 1}层:${val.join(' -- ')}`;
}).join('\n');
}
}