image.png

二叉堆数据结构

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

  • 结构特性:它是一棵完全二叉树,表示树的每一层都有左侧和右侧子节点(除了最后一层的叶节点),并且最后一层的叶节点尽可能都是左侧子节点。
  • 堆特性:二叉堆不是最小堆就是最大堆。最小堆允许你快速导出树的最小值,最大堆允许你快速导出数的最大值。

最大堆

父节点的键值总是大于或等于任何一个子节点的键值最大堆.png

最小堆

父节点的键值总是小于或等于任何一个子节点的键值最小堆.png

实现最小堆

创建最小堆类

在 constructor 构造函数中,定义 compareFn 函数来比较存储在数据结构中的值,使用数组来存储数据。

  1. class MinHeap {
  2. constructor(compareFn = defaultCompare) {
  3. this.compareFn = compareFn;
  4. this.heap = [];
  5. }
  6. }

声明 Compare 常量

  1. export const Compare = {
  2. LESS_THAN: -1,
  3. BIGGER_THAN: 1,
  4. EQUALS: 0
  5. };

创建 defaultCompare 函数

  1. export function defaultCompare(a, b) {
  2. if (a === b) {
  3. return Compare.EQUALS;
  4. }
  5. return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
  6. }

在本文中,我们使用数组来存储二叉堆的节点,因此我们可以通过 index 来获取二叉堆的节点。 对于给定位置 index 的节点:

  • 它的左侧子节点的位置是 2 * index + 1
  • 它的右侧子节点的位置是 2 * index + 2
  • 它的父节点位置 (index - 1)/ 2

根据以上规则,我们来实现获取节点的方法。

getLeftIndex - 获取左侧子节点

  1. getLeftIndex(index) {
  2. return (2 * index) + 1;
  3. }

getRightIndex - 获取右侧子节点

  1. getRightIndex(index) {
  2. return (2 * index) + 2;
  3. }

getParentIndex - 获取父节点

  1. getParentIndex(index) {
  2. if (index === 0) {
  3. return undefined;
  4. }
  5. return Math.floor((index - 1) / 2);
  6. }

insert - 向堆中插入值

  1. insert(value) {
  2. if (value != null) {
  3. const index = this.heap.length;
  4. // 将值插入到堆的底部叶节点(即数组的最后一个位置)
  5. this.heap.push(value);
  6. // 将插入的值和它的父节点进行交换,直到父节点小于这个插入的值
  7. this.siftUp(index);
  8. // 插入成功,返回 true
  9. return true;
  10. }
  11. // 插入失败,返回 false
  12. return false;
  13. }

将值插入到堆的底部叶节点 (即数组的最后一个位置),然后执行 siftUp 方法,将插入的值和它的父节点进行交换,直到父节点小于这个插入的值。

siftUp - 上移操作

  1. siftUp(index) {
  2. // 获取父节点的位置
  3. let parent = this.getParentIndex(index);
  4. while (
  5. index > 0 &&
  6. this.compareFn(this.heap[parent], this.heap[index]) === Compare.BIGGER_THAN
  7. ) {
  8. // 在最小堆中,如果插入的值小于它的父节点(或者在最大堆中比父节点大),将这个元素和父节点交换
  9. swap(this.heap, parent, index);
  10. // 交换节点位置
  11. index = parent;
  12. parent = this.getParentIndex(index);
  13. }
  14. }

swap 交换函数

  1. function swap(array, a, b) {
  2. const temp = array[a];
  3. array[a] = array[b];
  4. array[b] = temp;
  5. }

在 swap 函数中,定义了一个 temp 变量来缓存要交换的第一个元素,然后,将第二个元素赋值到第一个元素的位置 array[a] = array[b]; ,最后,将缓存起来的第一个元素的值覆盖到第二个元素的值 array[b] = temp;

也可以使用 ES6 的数组结构语法实现 swap 函数:

  1. const swap = (array, a, b) => [array[a], array[b]] = [array[b], array[a]];

size - 获取堆中数据的长度

  1. size() {
  2. return this.heap.length;
  3. }

isEmpty - 判断堆是否为空

  1. isEmpty() {
  2. return this.size() <= 0;
  3. }

clear - 清空堆

  1. this.heap = [];

findMinimum - 查找堆中最小值

  1. findMinimum() {
  2. return this.isEmpty() ? undefined : this.head[0];
  3. }

在最小堆中,最小值总是位于数组的第一个位置(堆的根节点)。在最大堆中,数组的第一个元素就是最大的值(堆的根节点),因此可以使用相同的代码获取最大堆的最大值。

extract - 导出堆中的最小值或最大值

  1. extract() {
  2. // 堆为空,也就是没有值可以导出,返回 undefined
  3. if (this.isEmpty()) {
  4. return undefined;
  5. }
  6. // 堆中只有一个值,直接移除并返回这个值
  7. if (this.size() === 1) {
  8. return this.heap.shift();
  9. }
  10. // 将需要移除的值缓存起来
  11. const removedValue = this.heap.shift();
  12. // 将堆的最后一个元素移动至根部
  13. this.heap[0] = this.heap.pop();
  14. // 从根元素开始交换元素,直至堆的结构正常
  15. this.siftDown(0);
  16. return removedValue;
  17. }

移除最小值(最小堆) 或 最大值(最大堆),也就是要移除数组中的第一个元素 (堆的根节点)。在移除后,我们需要将堆的最后一个元素移动至根部并执行 siftDown 函数,将堆中的元素进行交换直至堆的结构正常。

siftDown - 下移操作

  1. // 接收移除元素的位置作为参数
  2. siftDown(index) {
  3. let element = index;
  4. // 获取左侧子节点的位置
  5. const left = this.getLeftIndex(index);
  6. // 获取右侧子节点的位置
  7. const right = this.getRightIndex(index);
  8. // 获取堆的长度
  9. const size = this.size();
  10. // 如果元素 this.heap[element] 比它的左侧子节点要小,交换元素和它的左侧子节点
  11. if (
  12. left < size &&
  13. this.compareFn(this.heap[element], this.heap[left]) === Compare.BIGGER_THAN
  14. ) {
  15. element = left;
  16. }
  17. // 如果元素小于 this.heap[element] 小于它的右侧子节点,交换元素和它的右侧子节点
  18. if (
  19. right < size &&
  20. this.compareFn(this.heap[element], this.heap[right]) === Compare.BIGGER_THAN
  21. ) {
  22. element = right;
  23. }
  24. // 在找到最小子节点的位置后,校验它的值是否和 element 相同,不同,则进行交换
  25. if (index !== element) {
  26. swap(this.heap, index, element);
  27. this.siftDown(element);
  28. }
  29. }

heapify

  1. heapify(array) {
  2. if (array) {
  3. this.heap = array;
  4. }
  5. const maxIndex = Math.floor(this.size() / 2) - 1;
  6. for (let i = 0; i <= maxIndex; i++) {
  7. this.siftDown(i);
  8. }
  9. return this.heap;
  10. }

getAsArray

  1. getAsArray() {
  2. return this.heap;
  3. }

完整代码

  1. export class MinHeap {
  2. constructor(compareFn = defaultCompare) {
  3. this.compareFn = compareFn;
  4. this.heap = [];
  5. }
  6. // 获取左侧子节点位置
  7. getLeftIndex(index) {
  8. return (2 * index) + 1;
  9. }
  10. // 获取右侧子节点位置
  11. getRightIndex(index) {
  12. return (2 * index) + 2;
  13. }
  14. // 获取父节点位置
  15. getParentIndex(index) {
  16. if (index === 0) {
  17. return undefined;
  18. }
  19. return Math.floor((index - 1) / 2);
  20. }
  21. // 堆的大小
  22. size() {
  23. return this.heap.length;
  24. }
  25. // 是否为空堆
  26. isEmpty() {
  27. return this.size() <= 0;
  28. }
  29. // 清空堆
  30. clear() {
  31. this.heap = [];
  32. }
  33. // 查找堆的最小值、最大值
  34. findMinimum() {
  35. return this.isEmpty() ? undefined : this.heap[0];
  36. }
  37. // 向堆中插入值
  38. insert(value) {
  39. if (value != null) {
  40. const index = this.heap.length;
  41. this.heap.push(value);
  42. this.siftUp(index);
  43. return true;
  44. }
  45. return false;
  46. }
  47. // 下移操作
  48. siftDown(index) {
  49. let element = index;
  50. const left = this.getLeftIndex(index);
  51. const right = this.getRightIndex(index);
  52. const size = this.size();
  53. if (
  54. left < size &&
  55. this.compareFn(this.heap[element], this.heap[left]) === Compare.BIGGER_THAN
  56. ) {
  57. element = left;
  58. }
  59. if (
  60. right < size &&
  61. this.compareFn(this.heap[element], this.heap[right]) === Compare.BIGGER_THAN
  62. ) {
  63. element = right;
  64. }
  65. if (index !== element) {
  66. swap(this.heap, index, element);
  67. this.siftDown(element);
  68. }
  69. }
  70. // 节点上移操作
  71. siftUp(index) {
  72. let parent = this.getParentIndex(index);
  73. while (
  74. index > 0 &&
  75. this.compareFn(this.heap[parent], this.heap[index]) === Compare.BIGGER_THAN
  76. ) {
  77. swap(this.heap, parent, index);
  78. index = parent;
  79. parent = this.getParentIndex(index);
  80. }
  81. }
  82. // 导出堆中的最小值或最大值
  83. extract() {
  84. if (this.isEmpty()) {
  85. return undefined;
  86. }
  87. if (this.size() === 1) {
  88. return this.heap.shift();
  89. }
  90. const removedValue = this.heap[0];
  91. this.heap[0] = this.heap.pop();
  92. this.siftDown(0);
  93. return removedValue;
  94. }
  95. heapify(array) {
  96. if (array) {
  97. this.heap = array;
  98. }
  99. const maxIndex = Math.floor(this.size() / 2) - 1;
  100. for (let i = 0; i <= maxIndex; i++) {
  101. this.siftDown(i);
  102. }
  103. return this.heap;
  104. }
  105. getAsArray() {
  106. return this.heap;
  107. }
  108. }

实现最大堆

最大堆的实现和最小堆的实现是一模一样的,不同之处在于我们要把所有 > (大于) 的比较换成 < (小于) 的比较。

创建最大堆类

  1. class MaxHeap extends MinHeap {
  2. constructor (compareFn = defaultCompare) {
  3. super(compareFn);
  4. this.compareFn = reverseCompare(compareFn);
  5. }
  6. }

我们通过继承 MinHeap 类来实现最大堆,并定义了一个 reverseCompare 函数进行反向比较,也就是不再对 a 和 b 进行比较,而是将 b 和 a 进行比较。

reverseCompare

  1. function reverseCompare(compareFn) {
  2. return (a, b) => compareFn(b, a);
  3. }