覃超老师再讲解高级搜索时,提到 启发式搜索 也称 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)
具体实现
代码参考算法一书实现。
最大优先队列
class MaxPQ {constructor(compareTo = (a, b) => a - b) {this.compareTo = compareTo;this.N = 0;this.pq = [null]; // 位置0为哨兵}max() {return this.pq[1];}delMax() {const max = this.pq[1];// 把这个最大元素换到最后,删除之this.exch(1, this.N);this.pq[this.N] = null;this.N -= 1;// 让 pq[1] 下沉到正确位置this.sink(1);return max;}insert(e) {this.N += 1;// 先把新元素加到最后this.pq[this.N] = e;// 然后让它上浮到正确的位置this.swim(this.N);}get size() {return this.N;}/** 辅助函数 *//** 上浮第 k 个元素,以维护最大堆性质 */swim(k) {// 浮到堆顶,停止上浮while (k > 1 && this.less(this.parent(k), k)) {// 如果第k个元素比父级元素大,交换this.exch(k, this.parent(k));k = this.parent(k);}}/** 下沉第 k 个元素,以维护最大堆性质 */sink(k) {while (this.left(k) <= this.N) {// 先找出左节点let j = this.left(k);// 如果右节点存在,对比大小,取最大值if (j < this.N && this.less(j, j + 1)) {j += 1;}// 如果左右节点的最大值都比k小,就不必下沉了。if (this.less(j, k)) break;this.exch(j, k);k = j;}}exch(i, j) {const tmp = this.pq[i];this.pq[i] = this.pq[j];this.pq[j] = tmp;}/** pq[i] 是否比 pq[j] 小 */less(i, j) {return this.compareTo(this.pq[i], this.pq[j]) < 0;}parent(k) {return k >> 1;}left(k) {return k * 2;}right(k) {return k * 2 + 1;}}const maxpq = new MaxPQ();maxpq.insert(3); //maxpq.insert(6); //maxpq.insert(1); //maxpq.insert(8); //console.log(maxpq.max()); // 8maxpq.delMax(); // 8console.log(maxpq.max()); // 6maxpq.insert(11);console.log(maxpq.max()); // 11
最小优先队列
class MinPQ {constructor(compareTo = (a, b) => a - b) {this.compareTo = compareTo;this.N = 0;this.pq = [null]; // 位置0为哨兵}max() {return this.pq[1];}delMax() {const min = this.pq[1];this.exch(1, this.N);this.pq[this.N] = null;this.N -= 1;this.sink(1);return min;}insert(e) {this.N += 1;this.pq[this.N] = e;this.swim(this.N);}get size() {return this.N;}/** 辅助函数 *//** 上浮第 k 个元素,以维护最大堆性质 */swim(k) {while (k > 1 && this.greater(this.parent(k), k)) {// 如果第k个元素比父元素小,交换this.exch(k, this.parent(k));k = this.parent(k);}}/** 下沉第 k 个元素,以维护最大堆性质 */sink(k) {while (this.left(k) <= this.N) {let j = this.left(k);// 找出左右节点的最小节点if (j < this.N && this.greater(j, j + 1)) {j += 1;}// 如果k比两个元素都小,不必下沉if (this.greater(j, k)) break;this.exch(j, k);k = j;}}exch(i, j) {const tmp = this.pq[i];this.pq[i] = this.pq[j];this.pq[j] = tmp;}/** pq[i] 是否比 pq[j] 大 */greater(i, j) {return this.compareTo(this.pq[i], this.pq[j]) > 0;}parent(k) {return k >> 1;}left(k) {return k * 2;}right(k) {return k * 2 + 1;}}const minpq = new MinPQ();minpq.insert(3); //minpq.insert(6); //minpq.insert(1); //minpq.insert(8); //console.log(minpq.max()); // 1minpq.delMax(); // 1console.log(minpq.max()); // 3minpq.insert(2);console.log(minpq.max()); // 2
索引优先队列
优先队列有一个缺点,就是不能直接访问已存在于优先队列中的对象,并更新它们。
索引优先队用一个整数和对象进行关联,当我们需要更新该对象的值时,可以通这个整数进行快速索引,然后对对象的值进行更新(简而言之,用一个索引数组保存了对象在数组中的位置)。当然更新后的对象在优先队列中的位置可能发生变化,这样以保证整个队列还是一个优先队列。
实现原理
keys存放对象,pq存放与对象相关的整数,也就是对象在数组中的位置。qp存储与对象相关的整数在pq数组中的下标,目的是为了在修改k对应的对象时,能够快速找到pq中元素值对应的下标,同时在上浮/下沉的过程中同时维护它。
keys存放的对象不一定在数组中是连续存放的,qp同keys。pq是连续存放的,且作为优先队列,是需要通过swim/sink维护堆有序的。
在维护堆有序时,keys是不需要修改的,需要修改pq,qp
具体实现
代码参考算法一书实现。
索引最大优先队列
/*** 索引最大优先队列* 索引优先队列,用一个索引数组保存了元素在数组中的位置。* 对于pq是连续的,但是qp,keys是不连续的,*/class IndexMaxPQ {constructor(maxN, compareTo = (a, b) => a - b) {// `keys`存放对象,`pq`存放与对象相关的整数,也就是对象在数组中的位置。// `qp`存储与对象相关的整数在`pq`数组中的下标// qp[pq[i]] = i, pq[qp[j]] = jthis.compareTo = compareTo;this.maxN = maxN;this.N = 0;this.keys = new Array(maxN + 1);this.pq = new Array(maxN + 1);// 整数没对象没关联的时候,设置为-1this.qp = new Array(maxN + 1).fill(-1);}insert(i, key) {if (!this.validateIndex(i)) return;if (this.contains(i)) return;this.N += 1;this.pq[this.N] = i;this.qp[i] = this.N;this.keys[i] = key;this.swim(this.N);}change(i, key) {if (!this.validateIndex(i)) return;if (!this.contains(i)) return;this.keys[i] = key;// 修改对象后,通过上浮/下沉恢复堆有序this.swim(this.qp[i]);this.sink(this.qp[i]);}/** 删除整数i关联的对象 */delete(i) {if (!this.validateIndex(i)) return;if (!this.contains(i)) return;// i对应的对象和最后一个交换,上浮/下沉,删除最后一个const index = this.pq[i];this.exch(index, this.N);this.N -= 1;this.swim(index);this.sink(index);this.keys[i] = null;this.pq[i] = -1;}maxKey() {return this.keys[this.pq[1]];}maxIndex() {return this.pq[1];}/** 删除堆顶元素,并返回关联的整数 */delMax() {if (this.isEmpty()) return undefined;const index = this.maxIndex();// 堆顶和最后一个交换,之后堆顶下沉,删除最后的元素this.exch(1, this.N);this.N -= 1;this.sink(1);this.keys[index] = null;this.qp[index] = -1;this.pq[this.N + 1] = null;return index;}/** 查找有没有和整数i关联的对象 */contains(i) {return this.qp[i] !== -1;}/** 检查整数是否在有效 */validateIndex(i) {return i >= 0 && i < this.maxN;}isEmpty() {return this.N === 0;}get size() {return this.N;}exch(i, j) {const swap = this.pq[i];this.pq[i] = this.pq[j];this.pq[j] = swap;this.qp[this.pq[i]] = i;this.qp[this.pq[j]] = j;}less(i, j) {return this.compareTo(this.keys[this.pq[i]], this.keys[this.pq[j]]) < 0;}swim(i) {while (i > 1 && this.less(this.parent(i), i)) {// 如果i对应的对象大于父节点的,上浮this.exch(i, this.parent(i));i = this.parent(i);}}sink(i) {while (this.left(i) <= this.N) {let child = this.left(i);// 左右两个节点中找出最大的一个if (child < this.N && this.less(child, child + 1)) {child += 1;}// 如果i对应的对象没比最大的子节点小,那么无需下沉if (!this.less(i, child)) break;this.exch(i, child);i = child;}}parent(k) {return k >> 1;}left(k) {return k * 2;}right(k) {return k * 2 + 1;}}const strings = ['nkg', 'nmi', 'liba'];const strArr = strings.map((str) => str.split(''));const ipq = new IndexMaxPQ(strArr.length, (a, b) => a.charCodeAt(0) - b.charCodeAt(0));let ans = '';for (let i = 0; i < strArr.length; i += 1) {ipq.insert(i, strArr[i].shift());}while (!ipq.isEmpty()) {ans += ipq.maxKey();const i = ipq.delMax();const char = strArr[i].shift();if (char) {ipq.insert(i, char);}}console.log(ans);console.log(ans === 'nnmlkiigba');
索引最小优先队列
/*** 索引最小优先队列*/class IndexMinPQ {constructor(maxN, compareTo = (a, b) => a - b) {this.maxN = maxN;this.compareTo = compareTo;this.keys = new Array(maxN + 1);this.pq = new Array(maxN + 1);this.qp = new Array(maxN + 1).fill(-1);this.N = 0;}insert(i, key) {// 整数超过if (!this.validateIndex(i)) return;if (this.contains(i)) return;this.N += 1;this.pq[this.N] = i;this.qp[i] = this.N;this.keys[i] = key;this.swim(this.N);}change(i, key) {if (!this.validateIndex(i)) return;if (!this.contains(i)) return;this.keys[i] = key;// 修改对象之后,需要调整下堆有序,上浮或下沉,// 两个都操作一下,就不用判断具体是执行哪个操作了。this.swim(this.qp[i]);this.sink(this.qp[i]);}contains(i) {return this.qp[i] !== -1;}delete(i) {if (!this.validateIndex(i)) return;if (!this.contains(i)) return;// i对应的对象和最后一个交换,上浮/下沉,删除最后一个const index = this.qp[i];this.exch(this.pq, i, this.N);this.N -= 1;this.swim(index);this.sink(index);this.qp[i] = -1;this.keys[i] = null;}/** 返回堆顶对象 */minKey() {return this.keys[this.pq[1]];}/** 返回堆顶对象关联的整数 */minIndex() {return this.pq[1];}/** 删除堆顶对象,并返回堆顶对象关联的整数 */delMin() {if (this.isEmpty()) return undefined;const index = this.minIndex();// 堆顶和最后一个元素交换,然后删除最后一个元素this.exch(1, this.N);this.N -= 1;this.sink(1);this.keys[index] = null;this.qp[index] = -1;this.pq[this.N + 1] = null;return index;}isEmpty() {return this.N === 0;}get size() {return this.N;}swim(k) {while (k > 1 && this.greater(this.parent(k), k)) {this.exch(k, this.parent(k));k = this.parent(k);}}sink(k) {while (this.left(k) <= this.N) {let child = this.left(k);if (child < this.N && this.greater(child, child + 1)) {child += 1;}if (!this.greater(k, child)) break;this.exch(k, child);k = child;}}greater(i, j) {return this.compareTo(this.keys[this.pq[i]], this.keys[this.pq[j]]) > 0;}exch(i, j) {const swap = this.pq[i];this.pq[i] = this.pq[j];this.pq[j] = swap;this.qp[this.pq[i]] = i;this.qp[this.pq[j]] = j;}parent(k) {return k >> 1;}left(k) {return k * 2;}right(k) {return k * 2 + 1;}/** 检查整数是否在有效 */validateIndex(i) {return i >= 0 && i < this.maxN;}}const ipq = new IndexMinPQ(4, (a, b) => a.charCodeAt(0) - b.charCodeAt(0));ipq.insert(2, 'b');ipq.insert(0, 'e');ipq.insert(1, 's');ipq.insert(3, 't'); // pq = [2,0,1,3]console.log(ipq.minIndex()); // 2ipq.change(1, 'a');console.log(ipq.minIndex()); // 1console.log(ipq.delMin()); // 1// 多个有序字符串合并成一个有序字符串, 多项归并{const strings = ['afg', 'beh', 'ccdi'];const ipq = new IndexMinPQ(4, (a, b) => a.charCodeAt(0) - b.charCodeAt(0));const strArrs = strings.map((str) => str.split(''));for (let i = 0; i < strArrs.length; i += 1) {const char = strArrs[i].shift();ipq.insert(i, char);}let res = '';while (!ipq.isEmpty()) {res += ipq.minKey();const i = ipq.delMin();const char = strArrs[i].shift();if (char) ipq.insert(i, char);}console.log(res);console.log(res === 'abccdefghi');}
