二叉堆是计算机科学中一种很是著名的数据结构,因为它能高效、快速地找出最大值和最小值所以常被用于优先队列和堆排序算法。javascript
本文将详解二叉堆并用TypeScript将其实现,欢迎各位感兴趣的开发者阅读本文。java
本文重点讲解堆如何实现,对堆这种数据结构不了解的开发者请移步个人另外一篇文章:数据结构:堆git
二叉堆是一种特殊的二叉树,二叉堆也叫堆,它有如下两个特性:github
一颗彻底二叉树,它的每一层都有左侧和右侧子节点(除过最后一层的叶节点),而且最后一层的叶节点尽量都是左侧子节点。算法
下图描述了一颗彻底二叉树: typescript
下图描述了最大堆和最小堆 api
二叉堆有两种表现方式:数组
下图描述了两种不一样的表示方式 数据结构
咱们使用数组来表示二叉堆,对于给定位置(index)的节点,咱们能够对其进行以下操做:函数
向堆中插入数据(insert)是指将数据插入堆的底部叶节点再执行上移(siftUp), 表示咱们将要把这个数据和它的父节点进行交换,直到父节点小于这个插入的值。
上移操做的实现以下:
交换的实现以下:
接下来咱们用一个例子来描述上述插入过程,以下图所示为一个最小堆,咱们要插入一个新的节点2。
移除最小值(最小堆)或最大值(最大堆)表示移除数组中的第一个元素(堆的根节点)。
在移除后,咱们须要将堆的最后一个元素移动至根部并执行下移(siftDown)函数,表示咱们将交换元素直到堆的结构正常。
下移操做的实现:
接下来,咱们经过一个例子来说解上述执行过程,下图描述了一个最小堆
上述操做咱们实现了一个最小堆,最大堆与最小堆的别就在于节点的比较,所以咱们只须要继承最小堆,重写比对函数,将原来的a与b比较,改成b与a比较便可。
上面咱们讲解了堆的概念,分析了的实现思路,接下来咱们将上述实现思路转化为代码
export class MinHeap<T> {
// 用数组来描述一个堆
protected heap: T[];
constructor(protected compareFn: ICompareFunction<T> = defaultCompare) {
this.heap = [];
}
}
复制代码
// 获取左子节点的位置
protected getLeftIndex(index: number): number {
return 2 * index + 1;
}
// 获取右子节点的位置
protected getRightIndex(index: number): number {
return 2 * index + 2;
}
// 获取父节点的位置
protected getParentIndex(index: number): number | undefined {
if (index === 0) {
return undefined;
}
return Math.floor((index - 1) / 2);
}
复制代码
insert(value: T): boolean {
if (value != null) {
// 向堆的叶结点添加元素,即数组的尾部
this.heap.push(value);
// 进行上移操做,即上移节点至合适的位置
this.siftUp(this.heap.length - 1);
return true;
}
return false;
}
// 实现上移函数
protected siftUp(index: number): void {
// 获取父节点位置
let parent = <number>this.getParentIndex(index);
// 插入的位置必须大于0,且它的父节点大于其自己就执行循环里的操做
while (index > 0 && this.compareFn(this.heap[parent], this.heap[index]) === Compare.BIGGER_THAN) {
// 交换元素的位置
this.swap(this.heap, parent, index);
// 修改当前插入值的位置为它的父节点,从新获取父节点的位置,即重复这个过程直到堆的根节点也通过了交换
index = parent;
parent = <number>this.getParentIndex(index);
}
}
// 实现交换数组元素位置函数
protected swap(array: T[], exchangeElement: number, exchangedElement: number): void {
// 用一个临时变量保存交换元素
const temp = array[exchangeElement];
// 将被交换元素赋值给交换元素
array[exchangeElement] = array[exchangedElement];
// 将第一步保存的临时变量赋值给被交换元素
array[exchangedElement] = temp;
}
复制代码
findMinimum(): T | undefined {
// 返回数组的最小元素
return this.isEmpty() ? undefined : this.heap[0];
}
// 判断堆是否为空
isEmpty(): boolean {
return this.size() === 0;
}
复制代码
extract(): T | undefined {
if (this.isEmpty()) {
return undefined;
}
if (this.size() === 1) {
// 返回数组的第一个元素
return this.heap.shift();
}
const removedValue = this.heap.shift();
// 执行下移操做
this.siftDown(0);
return removedValue;
}
// 下移操做
protected siftDown(index: number): void {
// 保存当前插入值的位置
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.BIGGER_THAN) {
element = left;
}
// 元素有效,当前元素大于其右子节点
if (right < size && this.compareFn(this.heap[element], this.heap[right]) === Compare.BIGGER_THAN) {
element = right;
}
// 找到最小子节点的位置,校验它的值是否和element相同
if (index !== element) {
// 若是不相同将它和最小的element进行交换
this.swap(this.heap, index, element);
// 递归执行
this.siftDown(element);
}
}
复制代码
完整代码地址:Heap.ts
堆的一种应用就是堆排序,此处不讲解堆排序的实现思路,对堆排序不了解的开发者请移步个人另外一篇文章: 排序算法:堆排序的理解与实现
heapSort(array: T[]): void {
// 构建堆
this.buildHeap(array);
// 从堆的末尾开始遍历,将遍历到的元素与0好元素进行交换,而后执行下移操做
for (let i = array.length - 1; i >= 0; i--) {
this.swap(array, i, 0);
this.heapify(array, i, 0);
}
}
// 构建堆
private buildHeap(array: T[]) {
// 获取最后一个节点的位置
const last = array.length - 1;
const lastParent = <number>this.getParentIndex(last);
// 从最后一个节点的父节点开始进行heapify操做
for (let i = lastParent; i >= 0; i--) {
this.heapify(array, array.length, i);
}
}
// 交换节点
private heapify(array: T[], size: number, index: number) {
// 递归基线条件
if (index >= size) {
return false;
}
// 找到当前要操做节点的左、右子树
const left = this.getLeftIndex(index);
const right = this.getRightIndex(index);
// 保存当前要操做节点的位置
let element = index;
// 若是当前要操做节点的左子节点大于其父节点,更新element的值
if (left < size && this.compareFn(array[left], array[element]) === Compare.BIGGER_THAN) {
element = left;
}
// 若是当前要操做节点的右子节点大于其父节点,更新element的值
if (right < size && this.compareFn(array[right], array[element]) === Compare.BIGGER_THAN) {
element = right;
}
// element的位置不等于当前要操做节点,交换元素位置,递归执行
if (element !== index) {
this.swap(array, element, index);
this.heapify(array, size, element);
}
}
复制代码
接下来咱们测试下上述代码是否正常执行
import { MinHeap, MaxHeap } from "./lib/Heap.ts";
const minHeap = new MinHeap();
minHeap.insert(13);
minHeap.insert(10);
minHeap.insert(5);
minHeap.insert(7);
minHeap.insert(4);
minHeap.insert(17);
console.log("堆(min)的全部元素", minHeap.getIsArray());
console.log("堆(min)的最小值", minHeap.findMinimum());
console.log(minHeap.extract());
console.log(minHeap.getIsArray());
console.log("---------------------------------------");
const maxHeap = new MaxHeap();
maxHeap.insert(13);
maxHeap.insert(10);
maxHeap.insert(5);
maxHeap.insert(7);
maxHeap.insert(4);
maxHeap.insert(17);
console.log("堆(max)的全部元素", maxHeap.getIsArray());
console.log(maxHeap.extract());
console.log("堆(max)的最大值", maxHeap.findMinimum());
console.log("---------------------------------------");
const arrayTest = [12, 15, 17, 18, 4, 5, 1, 7, 19, 20];
minHeap.heapSort(arrayTest);
console.log(arrayTest);
复制代码