二叉堆和堆排序 二叉堆数据结构 (1)它是一棵完全二叉树,表示树的每一层都有左侧和右侧子节点(除了最后一层的叶节点),并且最后一层的叶节点尽可能都是左侧子节点,这叫作结构特性。 (2)二叉堆不是最小堆就是最大堆。最小堆允许你快速导出树的最小值,最大堆允许你快速导出树的最大值。所有的节点都大于等于(最大堆)或小于等于(最小堆)每个它的子节点。这叫作堆特性
二叉树有两种表示方式 第一种是使用一个动态的表示方式,也就是指针(用节点表示) 第二种是使用一个数组,通过索引值检索父节点、左侧和右侧子节点的值。
访问使用普通数组的二叉树节点: 它的左侧子节点的位置是 2 _ index + 1(如果位置可用); 它的右侧子节点的位置是 2 _ index + 2(如果位置可用); 它的父节点位置是 index / 2(如果位置可用)
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 82 83 84 85 86 87 88 89 90 91 import { defaultCompare } from '../util' ;export class MinHeap { constructor (compareFn = defaultCompare ) { this .compareFn = compareFn; this .heap = []; } getLeftIndex (index ) { return 2 * index + 1 ; } getRightIndex (index ) { return 2 * index + 2 ; } getParentIndex (index ) { if (index === 0 ) { return undefined ; } return Math .floor ((index - 1 ) / 2 ); } insert (value ) { if (value != null ) { this .heap .push (value); this .siftUp (this .heap .length - 1 ); return true ; } return false ; } siftUp (index ) { 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); } function swap (array, a, b ) { const temp = array[a]; array[a] = array[b]; array[b] = temp; } } size ( ) { return this .heap .length ; } isEmpty ( ) { return this .size () === 0 ; } findMinimum ( ) { return this .isEmpty () ? undefined : this .heap [0 ]; } extract ( ) { if (this .isEmpty ()) { return undefined ; } if (this .size () === 1 ) { return this .heap .shift (); } const removedValue = this .heap .shift (); this .siftDown (0 ); return removedValue; } siftDown (index ) { 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); } } }
创建最大堆类 MaxHeap 类的算法和 MinHeap 类的算法一模一样。不同之处在于我们要把所有>(大于)的比较换成<(小于)的比较。
1 2 3 4 5 6 7 8 9 10 function reverseCompare (compareFn ) { return (a, b ) => compareFn (b, a); } export class MaxHeap extends MinHeap { constructor (compareFn = defaultCompare ) { super (compareFn); this .compareFn = reverseCompare (compareFn); } }
堆排序算法 (1) 用数组创建一个最大堆用作源数据。 (2) 在创建最大堆后,最大的值会被存储在堆的第一个位置。我们要将它替换为堆的最后一个值,将堆的大小减 1。 (3) 最后,我们将堆的根节点下移并重复步骤 2 直到堆的大小为 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function heapSort (array, compareFn = defaultCompare ) { let heapSize = array.length ; buildMaxHeap (array, compareFn); while (heapSize > 1 ) { swap (array, 0 , --heapSize); heapify (array, 0 , heapSize, compareFn); } return array; } function buildMaxHeap (array, compareFn ) { for (let i = Math .floor (array.length / 2 ); i >= 0 ; i -= 1 ) { heapify (array, i, array.length , compareFn); } return array; }
heapify 函数和我们创建的 siftDown 方法有相同的代码。不同之处是我们会将堆本身、堆的大小和要使用的比较函数传入作为参数。这是因为我们不会直接使用堆数据结构,而是使用它的逻辑来开发 heapSort 算法
堆排序算法不是一个稳定的排序算法,也就是说如果数组没有排好序,可能会得到不一样的结果。
图 图的相关术语 图是网络结构的抽象模型。图是一组由边连接的节点(或顶点) G = (V, E) V: 一组顶点 E: 一组边,连接 V 中的顶点
由一条边连接在一起的顶点称为相邻顶点。比如,A 和 B 是相邻的,A 和 D 是相邻的,A 和 C 是相邻的,A 和 E 不是相邻的。 一个顶点的度是其相邻顶点的数量。比如,A 和其他三个顶点相连接,因此 A 的度为 3;E 和其他两个顶点相连,因此 E 的度为 2。 路径是顶点 v1, v2, …, vk 的一个连续序列,其中 vi 和 vi+1 是相邻的。以上一示意图中的图为例,其中包含路径 A B E I 和 A C D G。 简单路径要求不包含重复的顶点。举个例子,A D G 是一条简单路径。除去最后一个顶点(因为它和第一个顶点是同一个顶点),环也是一个简单路径,比如 A D C A(最后一个顶点重新回到 A)。 如果图中不存在环,则称该图是无环的。如果图中每两个顶点间都存在路径,则该图是连通的。
有向图和无向图 图可以是无向的(边没有方向)或是有向的(有向图)。 如果图中每两个顶点间在双向上都存在路径,则该图是强连通的。例如,C 和 D 是强连通的,而 A 和 B 不是强连通的。 图还可以是未加权的(目前为止我们看到的图都是未加权的)或是加权的。如下图所示,加权图的边被赋予了权值。
图的表示 邻接矩阵 每个节点都和一个整数相关联,该整数将作为数组的索引。用一个二维数组来表示顶点之间的连接。如果索引为 i 的节点和索引为 j 的节点相邻,则 array[i][j] === 1,否则 array[i][j] === 0缺点: (1)不是强连通的图(稀疏图)如果用邻接矩阵来表示,则矩阵中将会有很多 0,浪费了计算机存储空间来表示根本不存在的边。 (2)顶点的数量可能会改变,而二维数组不太灵活
邻接表 邻接表由图中每个顶点的相邻顶点列表所组成。存在好几种方式来表示这种数据结构。 可以用列表(数组)、链表,甚至是散列表或是字典来表示相邻顶点列表
关联矩阵 在关联矩阵中,矩阵的行表示顶点,列表示边。使用二维数组来表示两者之间的连通性,如果顶点 v 是边 e 的入射点,则 array[v][e] === 1;否则,array[v][e] === 0关联矩阵通常用于边的数量比顶点多的情况,以节省空间和内存
创建 Graph 类 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 class Graph { constructor (isDirected = false ) { this .isDirected = isDirected; this .vertices = []; this .adjList = new Dictionary (); } addVertex (v ) { if (!this .vertices .includes (v)) { this .vertices .push (v); this .adjList .set (v, []); } } addEdge (v, w ) { if (!this .adjList .get (v)) { this .addVertex (v); } if (!this .adjList .get (w)) { this .addVertex (w); } this .adjList .get (v).push (w); if (!this .isDirected ) { this .adjList .get (w).push (v); } } getVertices ( ) { return this .vertices ; } getAdjList ( ) { return this .adjList ; } toString ( ) { let s = '' ; for (let i = 0 ; i < this .vertices .length ; i++) { s += `${this .vertices[i]} -> ` ; const neighbors = this .adjList .get (this .vertices [i]); for (let j = 0 ; j < neighbors.length ; j++) { s += `${neighbors[j]} ` ; } s += '\n' ; } return s; } }
图的遍历 作用: 图遍历可以用来寻找特定的顶点或寻找两个顶点之间的路径,检查图是否连通,检查图是否含有环,等等 图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于两种图遍历算法,都需要明确指出第一个被访问的顶点。 完全探索一个顶点要求我们查看该顶点的每一条边。对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中。 为了保证算法的效率,务必访问每个顶点至多两次。连通图中每条边和顶点都会被访问到。
广度优先搜索(breadth-first search,BFS) 深度优先搜索(depth-first search,DFS) 不同点: 待访问顶点列表的数据结构 | 算 法 | 数据结构 | 描 述 | | ———— | ——– | ————————————————————– | | 深度优先搜索 | 栈 | 将顶点存入栈,顶点是沿着路径被探索的,存在新的相邻顶点就去访问 | | 广度优先搜索 | 队列 | 将顶点存入队列,最先入队列的顶点先被探索 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const Colors = { WHITE : 0 , GREY : 1 , BLACK : 2 , }; const initializeColor = (vertices ) => { const color = {}; for (let i = 0 ; i < vertices.length ; i++) { color[vertices[i]] = Colors .WHITE ; } return color; };
广度优先搜索 从指定的第一个顶点开始遍历图,先访问其所有的邻点(相邻顶点),就像一次访问图的一层。换句话说,就是先宽后深地访问顶点步骤 (1) 创建一个队列 Q。 (2) 标注 v 为被发现的(灰色),并将 v 入队列 Q。 (3) 如果 Q 非空,则运行以下步骤:
(a) 将 u 从 Q 中出队列;
(b) 标注 u 为被发现的(灰色);
(c) 将 u 所有未被访问过的邻点(白色)入队列;
(d) 标注 u 为已被探索的(黑色)
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 export const breadthFirstSearch = (graph, startVertex, callback ) => { const vertices = graph.getVertices (); const adjList = graph.getAdjList (); const color = initializeColor (vertices); const queue = new Queue (); queue.enqueue (startVertex); while (!queue.isEmpty ()) { const u = queue.dequeue (); const neighbors = adjList.get (u); color[u] = Colors .GREY ; for (let i = 0 ; i < neighbors.length ; i++) { const w = neighbors[i]; if (color[w] === Colors .WHITE ) { color[w] = Colors .GREY ; queue.enqueue (w); } } color[u] = Colors .BLACK ; if (callback) { callback (u); } } }; const printVertex = (value ) => console .log ('Visited vertex: ' + value); breadthFirstSearch (graph, myVertices[0 ], printVertex);
使用 BFS 寻找最短路径 给定一个图 G 和源顶点 v,找出每个顶点 u 和 v 之间最短路径的距离(以边的数量计)
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 const BFS = (graph, startVertex ) => { const vertices = graph.getVertices (); const adjList = graph.getAdjList (); const color = initializeColor (vertices); const queue = new Queue (); const distances = {}; const predecessors = {}; queue.enqueue (startVertex); for (let i = 0 ; i < vertices.length ; i++) { distances[vertices[i]] = 0 ; predecessors[vertices[i]] = null ; } while (!queue.isEmpty ()) { const u = queue.dequeue (); const neighbors = adjList.get (u); color[u] = Colors .GREY ; for (let i = 0 ; i < neighbors.length ; i++) { const w = neighbors[i]; if (color[w] === Colors .WHITE ) { color[w] = Colors .GREY ; distances[w] = distances[u] + 1 ; predecessors[w] = u; queue.enqueue (w); } } color[u] = Colors .BLACK ; } return { distances, predecessors, }; }; const shortestPathA = BFS (graph, myVertices[0 ]);console .log (shortestPathA);const fromVertex = myVertices[0 ]; for (i = 1 ; i < myVertices.length ; i++) { const toVertex = myVertices[i]; const path = new Stack (); for (let v = toVertex; v !== fromVertex; v = shortestPathA.predecessors [v]) { path.push (v); } path.push (fromVertex); let s = path.pop (); while (!path.isEmpty ()) { s += ' - ' + path.pop (); } console .log (s); }
深入学习最短路径算法 Dijkstra 算法解决了单源最短路径问题。 Bellman-Ford 算法解决了边权值为负的单源最短路径问题。 A*搜索算法解决了求仅一对顶点间的最短路径问题,用经验法则来加速搜索过程。 Floyd-Warshall 算法解决了求所有顶点对之间的最短路径这一问题
深度优先搜索 深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点
深度优先搜索算法不需要一个源顶点。在深度优先搜索算法中,若图中顶点 v 未访问,则访问该顶点 v。步骤 (1) 标注 v 为被发现的(灰色); (2) 对于 v 的所有未访问(白色)的邻点 w,访问顶点 w; (3) 标注 v 为已被探索的(黑色)。
深度优先搜索的步骤是递归的,这意味着深度优先搜索算法使用栈来存储函数调用(由递归调用所创建的栈)
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 const depthFirstSearch = (graph, callback ) => { const vertices = graph.getVertices (); const adjList = graph.getAdjList (); const color = initializeColor (vertices); for (let i = 0 ; i < vertices.length ; i++) { if (color[vertices[i]] === Colors .WHITE ) { depthFirstSearchVisit (vertices[i], color, adjList, callback); } } }; const depthFirstSearchVisit = (u, color, adjList, callback ) => { color[u] = Colors .GREY ; if (callback) { callback (u); } const neighbors = adjList.get (u); for (let i = 0 ; i < neighbors.length ; i++) { const w = neighbors[i]; if (color[w] === Colors .WHITE ) { depthFirstSearchVisit (w, color, adjList, callback); } } color[u] = Colors .BLACK ; }; depthFirstSearch (graph, printVertex);
Angular(版本 2+)在探测变更(验证 HTML 模板是否需要更新)方面使用的算法和深度优先搜索算法非常相似。
探索深度优先算法 对于给定的图 G,我们希望深度优先搜索算法遍历图 G 的所有节点,构建“森林”(有根树的一个集合)以及一组源顶点(根),并输出两个数组:发现时间和完成探索时间。
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 export const DFS = (graph ) => { const vertices = graph.getVertices (); const adjList = graph.getAdjList (); const color = initializeColor (vertices); const d = {}; const f = {}; const p = {}; const time = { count : 0 }; for (let i = 0 ; i < vertices.length ; i++) { f[vertices[i]] = 0 ; d[vertices[i]] = 0 ; p[vertices[i]] = null ; } for (let i = 0 ; i < vertices.length ; i++) { if (color[vertices[i]] === Colors .WHITE ) { DFSVisit (vertices[i], color, d, f, p, time, adjList); } } return { discovery : d, finished : f, predecessors : p, }; }; const DFSVisit = (u, color, d, f, p, time, adjList ) => { color[u] = Colors .GREY ; d[u] = ++time.count ; const neighbors = adjList.get (u); for (let i = 0 ; i < neighbors.length ; i++) { const w = neighbors[i]; if (color[w] === Colors .WHITE ) { p[w] = u; DFSVisit (w, color, d, f, p, time, adjList); } } color[u] = Colors .BLACK ; f[u] = ++time.count ; };
(1)时间(time)变量值的范围只可能在图顶点数量的一倍到两倍(2|V|)之间; (2)对于所有的顶点 u,d[u] < f[u] (意味着,发现时间的值比完成时间的值小,完成时间意思是所有顶点都已经被探索过了)。 在这两个假设下,我们有如下的规则。1 <= d [u] < f [u] <= 2|V| 如果对同一个图再跑一遍新的深度优先搜索方法,对图中每个顶点,我们会得到如下的发现/完成时间
拓扑排序——使用深度优先搜索 有向无环图(DAG)
需要编排一些任务或步骤的执行顺序时,称为拓扑排序(topological sorting,英文亦写作 topsort 或是 toposort)
拓扑排序只能应用于 DAG
1 2 3 4 5 6 7 8 9 10 11 12 graph = new Graph (true ); myVertices = ['A' , 'B' , 'C' , 'D' , 'E' , 'F' ]; for (i = 0 ; i < myVertices.length ; i++) { graph.addVertex (myVertices[i]); } graph.addEdge ('A' , 'C' ); graph.addEdge ('A' , 'D' ); graph.addEdge ('B' , 'D' ); graph.addEdge ('B' , 'E' ); graph.addEdge ('C' , 'F' ); graph.addEdge ('F' , 'E' ); const result = DFS (graph);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const fTimes = result.finished ;s = '' ; for (let count = 0 ; count < myVertices.length ; count++) { let max = 0 ; let maxName = null ; for (i = 0 ; i < myVertices.length ; i++) { if (fTimes[myVertices[i]] > max) { max = fTimes[myVertices[i]]; maxName = myVertices[i]; } } s += ' - ' + maxName; delete fTimes[maxName]; } console .log (s);
最短路径算法 Dijkstra 算法 Dijkstra 算法是一种计算从单个源到所有其他源的最短路径的贪心算法,这意味着我们可以用它来计算从图的一个顶点到其余各顶点的最短路径。
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 var graph = [ [0 , 2 , 4 , 0 , 0 , 0 ], [0 , 0 , 1 , 4 , 2 , 0 ], [0 , 0 , 0 , 0 , 3 , 0 ], [0 , 0 , 0 , 0 , 0 , 2 ], [0 , 0 , 0 , 3 , 0 , 2 ], [0 , 0 , 0 , 0 , 0 , 0 ], ]; const INF = Number .MAX_SAFE_INTEGER ;const dijkstra = (graph, src ) => { const dist = []; const visited = []; const { length } = graph; for (let i = 0 ; i < length; i++) { dist[i] = INF ; visited[i] = false ; } dist[src] = 0 ; for (let i = 0 ; i < length - 1 ; i++) { const u = minDistance (dist, visited); visited[u] = true ; for (let v = 0 ; v < length; v++) { if ( !visited[v] && graph[u][v] !== 0 && dist[u] !== INF && dist[u] + graph[u][v] < dist[v] ) { dist[v] = dist[u] + graph[u][v]; } } } return dist; }; const minDistance = (dist, visited ) => { let min = INF ; let minIndex = -1 ; for (let v = 0 ; v < dist.length ; v++) { if (visited[v] === false && dist[v] <= min) { min = dist[v]; minIndex = v; } } return minIndex; };
Floyd-Warshall 算法 Floyd-Warshall 算法是一种计算图中所有最短路径的动态规划算法。通过该算法,我们可以找出从所有源到所有顶点的最短路径。
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 const floydWarshall = (graph ) => { const dist = []; const { length } = graph; for (let i = 0 ; i < length; i++) { dist[i] = []; for (let j = 0 ; j < length; j++) { if (i === j) { dist[i][j] = 0 ; } else if (!isFinite (graph[i][j])) { dist[i][j] = Infinity ; } else { dist[i][j] = graph[i][j]; } } } for (let k = 0 ; k < length; k++) { for (let i = 0 ; i < length; i++) { for (let j = 0 ; j < length; j++) { if (dist[i][k] + dist[k][j] < dist[i][j]) { dist[i][j] = dist[i][k] + dist[k][j]; } } } } return dist; };
最小生成树(MST) Prim 算法 Prim 算法是一种求解加权无向连通图的 MST 问题的贪心算法。它能找出一个边的子集,使得其构成的树包含图中所有顶点,且边的权值之和最小。
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 var graph = [ [0 , 2 , 4 , 0 , 0 , 0 ], [2 , 0 , 2 , 4 , 2 , 0 ], [4 , 2 , 0 , 0 , 3 , 0 ], [0 , 4 , 0 , 0 , 3 , 2 ], [0 , 2 , 3 , 3 , 0 , 2 ], [0 , 0 , 0 , 2 , 2 , 0 ], ]; const INF = Number .MAX_SAFE_INTEGER ;const prim = (graph ) => { const parent = []; const key = []; const visited = []; const { length } = graph; for (let i = 0 ; i < length; i++) { key[i] = INF ; visited[i] = false ; } key[0 ] = 0 ; parent[0 ] = -1 ; for (let i = 0 ; i < length - 1 ; i++) { const u = minKey (graph, key, visited); visited[u] = true ; for (let v = 0 ; v < length; v++) { if (graph[u][v] && !visited[v] && graph[u][v] < key[v]) { parent[v] = u; key[v] = graph[u][v]; } } } return parent; }; const minDistance = (dist, key, visited ) => { let min = INF ; let minIndex = -1 ; for (let v = 0 ; v < dist[key].length ; v++) { if (visited[v] === false && dist[key][v] <= min) { min = dist[key][v]; minIndex = v; } } return minIndex; };
Kruskal 算法 是一种求加权无向连通图的 MST 的贪心算法
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 const kruskal = (graph ) => { const { length } = graph; const parent = []; let ne = 0 ; let a; let b; let u; let v; const cost = initializeCost (graph); while (ne < length - 1 ) { for (let i = 0 , min = INF ; i < length; i++) { for (let j = 0 ; j < length; j++) { if (cost[i][j] < min) { min = cost[i][j]; a = u = i; b = v = j; } } } u = find (u, parent); v = find (v, parent); if (union (u, v, parent)) { ne++; } cost[a][b] = cost[b][a] = INF ; } return parent; }; const find = (i, parent ) => { while (parent[i]) { i = parent[i]; } return i; }; const union = (i, j, parent ) => { if (i !== j) { parent[j] = i; return true ; } return false ; };