上一篇《TypeScript 数据结构与算法:红黑树》实现了 Typescript
中自平衡的红黑树
的数据结构与算法,本篇继续实现二叉堆(Heap)
。
二叉堆
二叉堆
是一种特殊的二叉树
,能高效、快速地找出最大值
和最小值
,常被应用于优先队列
,也被用于著名的堆排序算法中,它有两个特性:
结构特性
:它是一棵完全二叉树
,表示树的每一层都有左侧和右侧子节点(除了最后一层的叶节点),并且最后一层的叶节点尽可能都是左侧子节点。
堆特性
:二叉堆不是最小堆就是最大堆。最小堆允许你快速提取树的最小值,最大堆允许你快速提取树的最大值。所有的节点都大于等于(最大堆)或小于等于(最小堆)每个它的子节点。
如下图,满足堆有序
的完全二叉树
,也就是二叉堆:
二叉堆表示法
在之前表示二叉树数据结构时,都是使用的专门的树
数据结构,通过引用
来指向左右子节点
。但是由于二叉堆
是个完全二叉树
,所以可以使用更简单的数组
结构来实现,如下图所示,位置 k
的节点的父节点的位置为 ⌊k/2⌋
,而它的两个子节点的位置则分别为 2k
和 2k+1
。这样通过计算数组的索引
在就可以在树
中上下移动:从 a[k]
向上一层就令 k
等于 k/2
,向下一层则令 k
等于 2k
或 2k+1
。
数据结构实现
交换元素
首先实现一个辅助方法,用于交换
数组中的两个元素,使用了 ES6
的解构赋值
:
1 2 3 4 5 6
|
export function swap(array: any[], a: number, b: number) { [array[a], array[b]] = [array[b], array[a]]; }
|
二叉堆类
实现一个最小二叉堆 MinHeap
类。最大堆类似,只要将比较函数取反为 reverseCompare
即可。
在类中实现了一些基础方法,比如获取左子节点,右子节点和父节点索引
等方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| import { Compare, defaultCompare, ICompareFunction, reverseCompare, swap } from "../util";
export class MinHeap<T> { protected heap: T[] = [];
constructor(protected compareFn: ICompareFunction<T> = defaultCompare) {}
private getLeftIndex(index: number): number { return 2 * index + 1; }
private getRightIndex(index: number): number { return 2 * index + 2; }
private getParentIndex(index: number): number { if (index === 0) { return undefined; } return Math.floor((index - 1) / 2); }
size(): number { return this.heap.length; }
isEmpty(): boolean { return this.size() <= 0; }
clear() { this.heap = []; }
findMinimum(): T { return this.isEmpty() ? undefined : this.heap[0]; }
getAsArray() { return this.heap; } }
export class MaxHeap<T> extends MinHeap<T> { constructor(protected compareFn: ICompareFunction<T> = defaultCompare) { super(compareFn); this.compareFn = reverseCompare(compareFn); } }
|
上浮
如果由于某些操作,导致堆的某个节点变得比父节点更大,那么我们就需要通过交换它和父节点
来修复堆。如果交换后,仍然比祖父节点还大,就继续交换
,直到堆的有序状态恢复。如下图所示:
上浮
的实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
private siftUp(index: number): void { let parent = this.getParentIndex(index); while ( index > 0 && this.compareFn(this.heap[parent], this.heap[index]) === Compare.BIGGER_THAN ) { swap(this.heap, parent, index); index = parent; parent = this.getParentIndex(index); } }
|
下沉
同样,如果由于某些操作,导致堆的某个节点变得比子节点更小,那么我们就需要通过交换它和子节点
来修复堆。如果交换后,仍然比孙子节点还小,就继续交换
,直到堆的有序状态恢复。
但是下沉
操作时情况稍微复杂一点,可以选择向左
或者向右
下沉,毕竟二叉堆是个偏左
的完全二叉树,所以原则就是尽量向左下沉,不能向左下沉时再向右下沉。如下图所示:
下沉
的实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
private siftDown(index: number) { 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; }
if (index !== element) { swap(this.heap, index, element); this.siftDown(element); } }
|
插入元素和删除元素
讲完了上浮
和下沉
,那么具体是什么情况会导致二叉堆的失序
呢,其实就是两个操作,插入元素
和删除元素
。
向二叉堆中插入一个元素时,会先将元素添到数组的最后面
,然后根据情况开始上浮
。同样,删除元素时,只能删除根节点
也就是第一个元素
,并把最后一个元素
挪到第一个索引
处,然后根据情况下沉
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
insert(value: T): boolean { if (value != null) { const index = this.heap.length; this.heap.push(value); this.siftUp(index); return true; } return false; }
extract() { if (this.isEmpty()) { return undefined; } if (this.size() === 1) { return this.heap.shift(); } const removedValue = this.heap[0]; this.heap[0] = this.heap.pop(); this.siftDown(0); return removedValue; }
|
如此就能实现一个二叉堆
。
堆排序
讲完二叉堆的数据结构,再看下堆
在排序
中的应用。
由于最小堆的根节点始终为其所属树
的最小值
,所以如果排序一个给定数组
时,可以按如下两步操作:
- 使用
改写
的下沉方法
将给定数组堆化
,构造成一个二叉堆;
- 将二叉堆的根元素(
最小值
)与最后一个元素交换
,然后开始下沉
。再将根元素(第二小值
)与倒数第二个元素交换
,再下沉
。不断重复,直到完全排序
。
注意:第一步堆化
时,由于叶子节点
起码占了数组的一半
,所以只需要将数组的前一半元素
(也就是非叶子节点
)下沉即可,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| import { Compare, defaultCompare, ICompareFunction, swap } from "../../util";
function siftDown( array: any[], index: number, heapSize: number, compareFn: ICompareFunction<any> ) { let largest = index; const left = 2 * index + 1; const right = 2 * index + 2;
if ( left < heapSize && compareFn(array[left], array[index]) === Compare.BIGGER_THAN ) { largest = left; }
if ( right < heapSize && compareFn(array[right], array[largest]) === Compare.BIGGER_THAN ) { largest = right; }
if (largest !== index) { swap(array, index, largest); siftDown(array, largest, heapSize, compareFn); } }
export default function heapSort(array: any[], compareFn = defaultCompare) { let heapSize = array.length;
for (let i = Math.floor(array.length / 2); i >= 0; i -= 1) { siftDown(array, i, array.length, compareFn); }
while (heapSize > 1) { swap(array, 0, --heapSize); siftDown(array, 0, heapSize, compareFn); } return array; }
|
堆排序
已知唯一能够同时最优地利用空间和时间的方法 —— 原地排序
并且在最坏的情况下它也能保证使用 O(NlgN)
的时间复杂度。适用于空间十分紧张的嵌入式系统或低成本的移动设备。但它无法利用缓
存并且不是稳定排序
,所以现代系统很少使用。
下一篇来分析图
。
前端记事本,不定期更新,欢迎关注!