概念

首先,数据结构中的堆和堆内存完全不是一个东西。
参考知乎回答:https://www.zhihu.com/question/276016774/answer/385381844

这里,我们讨论数据结构:堆,英文叫,Heap。
堆是一种完全二叉树,它的特点是任何一个节点的子节点一定比其父节点大(或者小),如果整棵树都是比父节点大,它是一个最小堆,反之,则是最大堆。
image.png
这里需要理解的是,

  • 逻辑上,堆是一棵树,完全二叉树(就是除了最后一层节点外,上面的层次是一棵满二叉树)。
  • 另外,堆的每一对父子节点顺序是有要求的,要么都是大于关系,要么都是小于关系。

堆的实现

数据结构

从逻辑上来讲,堆是一棵树。但是完全二叉树可以用数组来表示。
比如上面图中所表示的堆,用数组表示就是:
数组:[ 6, 5, 3, 4, 2, 1 ]
下标: 0 1 2 3 4 5
现在要说的是,关于数组表示完全二叉树,有两个重要公式:
任意节点下标index而言, 它的父节点下标是:取整(index - 1 / 2);它的左边子节点是:index 2 + 1;它的右边子节点是:index 2 + 2;

就上面那幅图而言,我们用值是‘5’的节点举例子:
它的下标是1,那么父节点下标 (1 - 0) / 2, 取整为0,没问题,
左边子节点: 2 1 + 1 = 3,没问题,右边 2 1 + 2 = 4 ,也ok。

插入元素算法

堆的数据结构有个特点,就是我们在做任何增加删除操作的时候,都要维持堆的结构,就是保持父子节点的大小关系。
堆插入的算法思想是这样:
先向数组尾部追加一个元素。
找到这个元素的父元素,比较这个元素和父元素的大小关系是否正确,如果正确,那ok,按兵不动。如果不正确,那就要交换父元素和该元素,然后再比较新的父元素,这个过程可能持续到直到一直换到堆顶。
也就是说,最慢情况是,有多少层换了多少次。
n个节点的堆有log(n)层,所以时间复杂度,O(log(n))

删除堆顶算法

删除堆顶的操作,我们的思想是:
先把堆中最后一个元素的弹出,把它的值设置为堆顶。
这时候,堆的稳定性破坏了,但是除了堆顶,其他的节点都ok。
所以,堆顶和它的子节点比较,如果不符合大小关系就换一下,换了以后和新的子节点再比较,不行再换….最终我们就得到删除堆顶以后的一个新的堆。
时间复杂度,依旧,O(log(n))

实现,用TS以及可以做的更多

用TS实现了一个最小堆,不过既然用TS了,那就可以做很多事情了:

  1. /**
  2. * 简单实现最小堆类
  3. */
  4. class MinHeap {
  5. private heap: Array<number>;
  6. constructor() {
  7. this.heap = [];
  8. }
  9. // 交换两个元素
  10. private swap(aIndex: number, bIndex: number) {
  11. const tmp = this.heap[aIndex];
  12. this.heap[aIndex] = this.heap[bIndex];
  13. this.heap[bIndex] = tmp;
  14. }
  15. // 得到父节点公式是:子节点下标 - 1 / 2,取整
  16. private getParentIndex(index: number) {
  17. return Math.floor((index - 1) / 2);
  18. }
  19. // 得到左右子节点公式
  20. // 左子 = index * 2 + 1
  21. // 右子 = index * 2 + 2
  22. private getLeftChildIndex(index: number) {
  23. return index * 2 + 1;
  24. }
  25. private getRightChildIndex(index: number) {
  26. return index * 2 + 2;
  27. }
  28. // 上移到合适的位置的方法
  29. // 和父节点交换
  30. private shiftUp(index: number) {
  31. if (index === 0) return;
  32. const parentIndex = this.getParentIndex(index);
  33. if (this.heap[index] < this.heap[parentIndex]) {
  34. this.swap(index, parentIndex);
  35. this.shiftUp(parentIndex);
  36. }
  37. }
  38. // 下沉到正确的位置
  39. // 和子节点交换
  40. private shiftDown(index: number) {
  41. const leftChildIndex = this.getLeftChildIndex(index);
  42. const rightChildIndex = this.getRightChildIndex(index);
  43. if(this.heap[index] > this.heap[leftChildIndex]) {
  44. this.swap(index, leftChildIndex);
  45. this.shiftDown(leftChildIndex);
  46. }
  47. if(this.heap[index] > this.heap[rightChildIndex]){
  48. this.swap(index, rightChildIndex);
  49. this.shiftDown(rightChildIndex);
  50. }
  51. }
  52. /**
  53. * 插入的方法
  54. * 先插入,后上移动到合适的位置
  55. */
  56. public insert(value: number) {
  57. // 先插入
  58. this.heap.push(value);
  59. // 上移
  60. this.shiftUp(this.heap.length - 1);
  61. }
  62. /**
  63. * 删除堆顶的方法
  64. * 先把最后一个元素放在堆顶
  65. * 然后让堆顶元素下沉到合适位置
  66. */
  67. public pop() {
  68. if (this.heap.length === 1) {
  69. this.heap.pop();
  70. return;
  71. }
  72. const last = this.heap.pop();
  73. if (last) {
  74. this.heap[0] = last;
  75. this.shiftDown(0);
  76. }
  77. }
  78. /**
  79. * 得到对顶元素的值
  80. */
  81. public peak() {
  82. return this.heap[0];
  83. }
  84. /**
  85. * 得到堆的大小
  86. */
  87. public size() {
  88. return this.heap.length;
  89. }
  90. }
  91. //////////// test
  92. const h = new MinHeap();
  93. h.insert(3);
  94. h.insert(2);
  95. h.insert(1);
  96. h.insert(10);
  97. h.pop()

这里有一个番外篇:实战:TS面向对象实现堆数据结构(Heap) (写于等飞机时)

面试题

待补充