覃超老师再讲解高级搜索时,提到 启发式搜索 也称 A搜索。这一开始我以为搜索就是等于A

https://leetcode-cn.com/problems/shortest-path-in-binary-matrix
在这道最短路径的题目的时候,对两者产生一个疑问🤔️?两者是等同的吗。还有算法中涉及到的优先队列,二叉堆等数据结构,由于本人本身是算法菜鸡,一时间搞混了。为了更好的理解,决定缕清楚上面说到的知识点。

启发式

heuristic可认为是智能搜索,或者是根据某一项条件来不断优化搜索的方向,也就是说一边搜索一边思考。本质是通过优先级不断地去找要找到的点,先用优先级高的拿出来搜索即可。自然而然的就是用优先级队列存储节点。

启发式函数是一种告知搜索方向的方法。它提供了一种明知的方法来猜测哪个邻节点会导向一个目标。

A*算法

A算法是启发式搜索的一种,其中A的启发式函数定义如下:

f(n) = g(n) + h(n)

g(n)表示起到到顶点n的开销,实际距离
h(n)表示顶点n到终点的估算距离(根据所采用的评估函数的不同而变化)。常见的评估函数有——欧几里得距离曼哈顿距离切比雪夫距离

如果不计算g(n),也就是g(n) = 0,则算法转换成Greedy Best-First Search,每一步选择距离终点最近的节点,期待最终结果是最优的。实际所得路径未必是最短路径。

如果不计算h(n),也就是h(n) = 0,则算法转换成为单源最短路径问题,即Dijkstra算法。它只考虑起点到顶点n的实际距离(开销),算法必定是能找到最短路径,但效率没有A高,节点的扩散范围比A大。

优先队列

启发式搜索本质就是找优先级高的处理搜索,自然就是要用到优先队列的数据结构。

优先队列有几种实现方式。

初级实现

  • 有序数组实现:插入O(n),取出O(1)
  • 无序数组实现:插入O(1),取出O(n)
  • 链表实现

典型实现

优先队列往往是用(二叉)堆来实现的。二叉堆实现的优先队列保证插入取出的时间复杂度都是O(logn)

二叉堆

二叉堆为一种特殊的二叉树,往往是用数组实现。二叉堆分为最大堆,和最小堆。

最大堆的性质是每个节点都大于等于它的两个子节点

最小堆的性质是每个节点都小于等于它的两个子节点

二叉堆的删除和插入时间复杂度为O(logn)

具体实现

代码参考算法一书实现。

最大优先队列

  1. class MaxPQ {
  2. constructor(compareTo = (a, b) => a - b) {
  3. this.compareTo = compareTo;
  4. this.N = 0;
  5. this.pq = [null]; // 位置0为哨兵
  6. }
  7. max() {
  8. return this.pq[1];
  9. }
  10. delMax() {
  11. const max = this.pq[1];
  12. // 把这个最大元素换到最后,删除之
  13. this.exch(1, this.N);
  14. this.pq[this.N] = null;
  15. this.N -= 1;
  16. // 让 pq[1] 下沉到正确位置
  17. this.sink(1);
  18. return max;
  19. }
  20. insert(e) {
  21. this.N += 1;
  22. // 先把新元素加到最后
  23. this.pq[this.N] = e;
  24. // 然后让它上浮到正确的位置
  25. this.swim(this.N);
  26. }
  27. get size() {
  28. return this.N;
  29. }
  30. /** 辅助函数 */
  31. /** 上浮第 k 个元素,以维护最大堆性质 */
  32. swim(k) {
  33. // 浮到堆顶,停止上浮
  34. while (k > 1 && this.less(this.parent(k), k)) {
  35. // 如果第k个元素比父级元素大,交换
  36. this.exch(k, this.parent(k));
  37. k = this.parent(k);
  38. }
  39. }
  40. /** 下沉第 k 个元素,以维护最大堆性质 */
  41. sink(k) {
  42. while (this.left(k) <= this.N) {
  43. // 先找出左节点
  44. let j = this.left(k);
  45. // 如果右节点存在,对比大小,取最大值
  46. if (j < this.N && this.less(j, j + 1)) {
  47. j += 1;
  48. }
  49. // 如果左右节点的最大值都比k小,就不必下沉了。
  50. if (this.less(j, k)) break;
  51. this.exch(j, k);
  52. k = j;
  53. }
  54. }
  55. exch(i, j) {
  56. const tmp = this.pq[i];
  57. this.pq[i] = this.pq[j];
  58. this.pq[j] = tmp;
  59. }
  60. /** pq[i] 是否比 pq[j] 小 */
  61. less(i, j) {
  62. return this.compareTo(this.pq[i], this.pq[j]) < 0;
  63. }
  64. parent(k) {
  65. return k >> 1;
  66. }
  67. left(k) {
  68. return k * 2;
  69. }
  70. right(k) {
  71. return k * 2 + 1;
  72. }
  73. }
  74. const maxpq = new MaxPQ();
  75. maxpq.insert(3); //
  76. maxpq.insert(6); //
  77. maxpq.insert(1); //
  78. maxpq.insert(8); //
  79. console.log(maxpq.max()); // 8
  80. maxpq.delMax(); // 8
  81. console.log(maxpq.max()); // 6
  82. maxpq.insert(11);
  83. console.log(maxpq.max()); // 11

最小优先队列

  1. class MinPQ {
  2. constructor(compareTo = (a, b) => a - b) {
  3. this.compareTo = compareTo;
  4. this.N = 0;
  5. this.pq = [null]; // 位置0为哨兵
  6. }
  7. max() {
  8. return this.pq[1];
  9. }
  10. delMax() {
  11. const min = this.pq[1];
  12. this.exch(1, this.N);
  13. this.pq[this.N] = null;
  14. this.N -= 1;
  15. this.sink(1);
  16. return min;
  17. }
  18. insert(e) {
  19. this.N += 1;
  20. this.pq[this.N] = e;
  21. this.swim(this.N);
  22. }
  23. get size() {
  24. return this.N;
  25. }
  26. /** 辅助函数 */
  27. /** 上浮第 k 个元素,以维护最大堆性质 */
  28. swim(k) {
  29. while (k > 1 && this.greater(this.parent(k), k)) {
  30. // 如果第k个元素比父元素小,交换
  31. this.exch(k, this.parent(k));
  32. k = this.parent(k);
  33. }
  34. }
  35. /** 下沉第 k 个元素,以维护最大堆性质 */
  36. sink(k) {
  37. while (this.left(k) <= this.N) {
  38. let j = this.left(k);
  39. // 找出左右节点的最小节点
  40. if (j < this.N && this.greater(j, j + 1)) {
  41. j += 1;
  42. }
  43. // 如果k比两个元素都小,不必下沉
  44. if (this.greater(j, k)) break;
  45. this.exch(j, k);
  46. k = j;
  47. }
  48. }
  49. exch(i, j) {
  50. const tmp = this.pq[i];
  51. this.pq[i] = this.pq[j];
  52. this.pq[j] = tmp;
  53. }
  54. /** pq[i] 是否比 pq[j] 大 */
  55. greater(i, j) {
  56. return this.compareTo(this.pq[i], this.pq[j]) > 0;
  57. }
  58. parent(k) {
  59. return k >> 1;
  60. }
  61. left(k) {
  62. return k * 2;
  63. }
  64. right(k) {
  65. return k * 2 + 1;
  66. }
  67. }
  68. const minpq = new MinPQ();
  69. minpq.insert(3); //
  70. minpq.insert(6); //
  71. minpq.insert(1); //
  72. minpq.insert(8); //
  73. console.log(minpq.max()); // 1
  74. minpq.delMax(); // 1
  75. console.log(minpq.max()); // 3
  76. minpq.insert(2);
  77. console.log(minpq.max()); // 2

索引优先队列

优先队列有一个缺点,就是不能直接访问已存在于优先队列中的对象,并更新它们。

索引优先队用一个整数和对象进行关联,当我们需要更新该对象的值时,可以通这个整数进行快速索引,然后对对象的值进行更新(简而言之,用一个索引数组保存了对象在数组中的位置)。当然更新后的对象在优先队列中的位置可能发生变化,这样以保证整个队列还是一个优先队列。

实现原理

keys存放对象,pq存放与对象相关的整数,也就是对象在数组中的位置。
qp存储与对象相关的整数在pq数组中的下标,目的是为了在修改k对应的对象时,能够快速找到pq中元素值对应的下标,同时在上浮/下沉的过程中同时维护它。

keys存放的对象不一定在数组中是连续存放的,qpkeyspq是连续存放的,且作为优先队列,是需要通过swim/sink维护堆有序的。
在维护堆有序时,keys是不需要修改的,需要修改pqqp

具体实现

代码参考算法一书实现。

索引最大优先队列

  1. /**
  2. * 索引最大优先队列
  3. * 索引优先队列,用一个索引数组保存了元素在数组中的位置。
  4. * 对于pq是连续的,但是qp,keys是不连续的,
  5. */
  6. class IndexMaxPQ {
  7. constructor(maxN, compareTo = (a, b) => a - b) {
  8. // `keys`存放对象,`pq`存放与对象相关的整数,也就是对象在数组中的位置。
  9. // `qp`存储与对象相关的整数在`pq`数组中的下标
  10. // qp[pq[i]] = i, pq[qp[j]] = j
  11. this.compareTo = compareTo;
  12. this.maxN = maxN;
  13. this.N = 0;
  14. this.keys = new Array(maxN + 1);
  15. this.pq = new Array(maxN + 1);
  16. // 整数没对象没关联的时候,设置为-1
  17. this.qp = new Array(maxN + 1).fill(-1);
  18. }
  19. insert(i, key) {
  20. if (!this.validateIndex(i)) return;
  21. if (this.contains(i)) return;
  22. this.N += 1;
  23. this.pq[this.N] = i;
  24. this.qp[i] = this.N;
  25. this.keys[i] = key;
  26. this.swim(this.N);
  27. }
  28. change(i, key) {
  29. if (!this.validateIndex(i)) return;
  30. if (!this.contains(i)) return;
  31. this.keys[i] = key;
  32. // 修改对象后,通过上浮/下沉恢复堆有序
  33. this.swim(this.qp[i]);
  34. this.sink(this.qp[i]);
  35. }
  36. /** 删除整数i关联的对象 */
  37. delete(i) {
  38. if (!this.validateIndex(i)) return;
  39. if (!this.contains(i)) return;
  40. // i对应的对象和最后一个交换,上浮/下沉,删除最后一个
  41. const index = this.pq[i];
  42. this.exch(index, this.N);
  43. this.N -= 1;
  44. this.swim(index);
  45. this.sink(index);
  46. this.keys[i] = null;
  47. this.pq[i] = -1;
  48. }
  49. maxKey() {
  50. return this.keys[this.pq[1]];
  51. }
  52. maxIndex() {
  53. return this.pq[1];
  54. }
  55. /** 删除堆顶元素,并返回关联的整数 */
  56. delMax() {
  57. if (this.isEmpty()) return undefined;
  58. const index = this.maxIndex();
  59. // 堆顶和最后一个交换,之后堆顶下沉,删除最后的元素
  60. this.exch(1, this.N);
  61. this.N -= 1;
  62. this.sink(1);
  63. this.keys[index] = null;
  64. this.qp[index] = -1;
  65. this.pq[this.N + 1] = null;
  66. return index;
  67. }
  68. /** 查找有没有和整数i关联的对象 */
  69. contains(i) {
  70. return this.qp[i] !== -1;
  71. }
  72. /** 检查整数是否在有效 */
  73. validateIndex(i) {
  74. return i >= 0 && i < this.maxN;
  75. }
  76. isEmpty() {
  77. return this.N === 0;
  78. }
  79. get size() {
  80. return this.N;
  81. }
  82. exch(i, j) {
  83. const swap = this.pq[i];
  84. this.pq[i] = this.pq[j];
  85. this.pq[j] = swap;
  86. this.qp[this.pq[i]] = i;
  87. this.qp[this.pq[j]] = j;
  88. }
  89. less(i, j) {
  90. return this.compareTo(this.keys[this.pq[i]], this.keys[this.pq[j]]) < 0;
  91. }
  92. swim(i) {
  93. while (i > 1 && this.less(this.parent(i), i)) {
  94. // 如果i对应的对象大于父节点的,上浮
  95. this.exch(i, this.parent(i));
  96. i = this.parent(i);
  97. }
  98. }
  99. sink(i) {
  100. while (this.left(i) <= this.N) {
  101. let child = this.left(i);
  102. // 左右两个节点中找出最大的一个
  103. if (child < this.N && this.less(child, child + 1)) {
  104. child += 1;
  105. }
  106. // 如果i对应的对象没比最大的子节点小,那么无需下沉
  107. if (!this.less(i, child)) break;
  108. this.exch(i, child);
  109. i = child;
  110. }
  111. }
  112. parent(k) {
  113. return k >> 1;
  114. }
  115. left(k) {
  116. return k * 2;
  117. }
  118. right(k) {
  119. return k * 2 + 1;
  120. }
  121. }
  122. const strings = ['nkg', 'nmi', 'liba'];
  123. const strArr = strings.map((str) => str.split(''));
  124. const ipq = new IndexMaxPQ(strArr.length, (a, b) => a.charCodeAt(0) - b.charCodeAt(0));
  125. let ans = '';
  126. for (let i = 0; i < strArr.length; i += 1) {
  127. ipq.insert(i, strArr[i].shift());
  128. }
  129. while (!ipq.isEmpty()) {
  130. ans += ipq.maxKey();
  131. const i = ipq.delMax();
  132. const char = strArr[i].shift();
  133. if (char) {
  134. ipq.insert(i, char);
  135. }
  136. }
  137. console.log(ans);
  138. console.log(ans === 'nnmlkiigba');

索引最小优先队列

  1. /**
  2. * 索引最小优先队列
  3. */
  4. class IndexMinPQ {
  5. constructor(maxN, compareTo = (a, b) => a - b) {
  6. this.maxN = maxN;
  7. this.compareTo = compareTo;
  8. this.keys = new Array(maxN + 1);
  9. this.pq = new Array(maxN + 1);
  10. this.qp = new Array(maxN + 1).fill(-1);
  11. this.N = 0;
  12. }
  13. insert(i, key) {
  14. // 整数超过
  15. if (!this.validateIndex(i)) return;
  16. if (this.contains(i)) return;
  17. this.N += 1;
  18. this.pq[this.N] = i;
  19. this.qp[i] = this.N;
  20. this.keys[i] = key;
  21. this.swim(this.N);
  22. }
  23. change(i, key) {
  24. if (!this.validateIndex(i)) return;
  25. if (!this.contains(i)) return;
  26. this.keys[i] = key;
  27. // 修改对象之后,需要调整下堆有序,上浮或下沉,
  28. // 两个都操作一下,就不用判断具体是执行哪个操作了。
  29. this.swim(this.qp[i]);
  30. this.sink(this.qp[i]);
  31. }
  32. contains(i) {
  33. return this.qp[i] !== -1;
  34. }
  35. delete(i) {
  36. if (!this.validateIndex(i)) return;
  37. if (!this.contains(i)) return;
  38. // i对应的对象和最后一个交换,上浮/下沉,删除最后一个
  39. const index = this.qp[i];
  40. this.exch(this.pq, i, this.N);
  41. this.N -= 1;
  42. this.swim(index);
  43. this.sink(index);
  44. this.qp[i] = -1;
  45. this.keys[i] = null;
  46. }
  47. /** 返回堆顶对象 */
  48. minKey() {
  49. return this.keys[this.pq[1]];
  50. }
  51. /** 返回堆顶对象关联的整数 */
  52. minIndex() {
  53. return this.pq[1];
  54. }
  55. /** 删除堆顶对象,并返回堆顶对象关联的整数 */
  56. delMin() {
  57. if (this.isEmpty()) return undefined;
  58. const index = this.minIndex();
  59. // 堆顶和最后一个元素交换,然后删除最后一个元素
  60. this.exch(1, this.N);
  61. this.N -= 1;
  62. this.sink(1);
  63. this.keys[index] = null;
  64. this.qp[index] = -1;
  65. this.pq[this.N + 1] = null;
  66. return index;
  67. }
  68. isEmpty() {
  69. return this.N === 0;
  70. }
  71. get size() {
  72. return this.N;
  73. }
  74. swim(k) {
  75. while (k > 1 && this.greater(this.parent(k), k)) {
  76. this.exch(k, this.parent(k));
  77. k = this.parent(k);
  78. }
  79. }
  80. sink(k) {
  81. while (this.left(k) <= this.N) {
  82. let child = this.left(k);
  83. if (child < this.N && this.greater(child, child + 1)) {
  84. child += 1;
  85. }
  86. if (!this.greater(k, child)) break;
  87. this.exch(k, child);
  88. k = child;
  89. }
  90. }
  91. greater(i, j) {
  92. return this.compareTo(this.keys[this.pq[i]], this.keys[this.pq[j]]) > 0;
  93. }
  94. exch(i, j) {
  95. const swap = this.pq[i];
  96. this.pq[i] = this.pq[j];
  97. this.pq[j] = swap;
  98. this.qp[this.pq[i]] = i;
  99. this.qp[this.pq[j]] = j;
  100. }
  101. parent(k) {
  102. return k >> 1;
  103. }
  104. left(k) {
  105. return k * 2;
  106. }
  107. right(k) {
  108. return k * 2 + 1;
  109. }
  110. /** 检查整数是否在有效 */
  111. validateIndex(i) {
  112. return i >= 0 && i < this.maxN;
  113. }
  114. }
  115. const ipq = new IndexMinPQ(4, (a, b) => a.charCodeAt(0) - b.charCodeAt(0));
  116. ipq.insert(2, 'b');
  117. ipq.insert(0, 'e');
  118. ipq.insert(1, 's');
  119. ipq.insert(3, 't'); // pq = [2,0,1,3]
  120. console.log(ipq.minIndex()); // 2
  121. ipq.change(1, 'a');
  122. console.log(ipq.minIndex()); // 1
  123. console.log(ipq.delMin()); // 1
  124. // 多个有序字符串合并成一个有序字符串, 多项归并
  125. {
  126. const strings = ['afg', 'beh', 'ccdi'];
  127. const ipq = new IndexMinPQ(4, (a, b) => a.charCodeAt(0) - b.charCodeAt(0));
  128. const strArrs = strings.map((str) => str.split(''));
  129. for (let i = 0; i < strArrs.length; i += 1) {
  130. const char = strArrs[i].shift();
  131. ipq.insert(i, char);
  132. }
  133. let res = '';
  134. while (!ipq.isEmpty()) {
  135. res += ipq.minKey();
  136. const i = ipq.delMin();
  137. const char = strArrs[i].shift();
  138. if (char) ipq.insert(i, char);
  139. }
  140. console.log(res);
  141. console.log(res === 'abccdefghi');
  142. }

资料