在计算机科学中,图是一种网络结构的抽象模型,它是一组由边链接的顶点组成。一个图G = (V, E)由如下元素组成:html
下图表示了一个图的结构:算法
在介绍如何用JavaScript实现图以前,咱们先介绍一些和图相关的术语。数组
如上图所示,由一条边链接在一块儿的顶点称为相邻顶点,A和B是相邻顶点,A和D是相邻顶点,A和C是相邻顶点......A和E是不相邻顶点。一个顶点的度是其相邻顶点的数量,A和其它三个顶点相连,因此A的度为3,E和其它两个顶点相连,因此E的度为2......路径是一组相邻顶点的连续序列,如上图中包含路径ABEI、路径ACDG、路径ABE、路径ACDH等。简单路径要求路径中不包含有重复的顶点,若是将环的最后一个顶点去掉,它也是一个简单路径。例如路径ADCA是一个环,它不是一个简单路径,若是将路径中的最后一个顶点A去掉,那么它就是一个简单路径。若是图中不存在环,则称该图是无环的。若是图中任何两个顶点间都存在路径,则该图是连通的,如上图就是一个连通图。若是图的边没有方向,则该图是无向图,上图所示为无向图,反之则称为有向图,下图所示为有向图:网络
在有向图中,若是两个顶点间在双向上都存在路径,则称这两个顶点是强连通的,如上图中C和D是强连通的,而A和B是非强连通的。若是有向图中的任何两个顶点间在双向上都存在路径,则该有向图是强连通的,非强连通的图也称为稀疏图。数据结构
此外,图还能够是加权的。前面咱们看到的图都是未加权的,下图为一个加权的图:ide
能够想象一下,前面咱们介绍的树和链表也属于图的一种特殊形式。图在计算机科学中的应用十分普遍,例如咱们能够搜索图中的一个特定顶点或一条特定的边,或者寻找两个顶点间的路径以及最短路径,检测图中是否存在环等等。函数
存在多种不一样的方式来实现图的数据结构,下面介绍几种经常使用的方式。测试
在邻接矩阵中,咱们用一个二维数组来表示图中顶点之间的链接,若是两个顶点之间存在链接,则这两个顶点对应的二维数组下标的元素的值为1,不然为0。下图是用邻接矩阵方式表示的图:this
若是是加权的图,咱们能够将邻接矩阵中二维数组里的值1改为对应的加权数。邻接矩阵方式存在一个缺点,若是图是非强连通的,则二维数组中会有不少的0,这表示咱们使用了不少的存储空间来表示根本不存在的边。另外一个缺点就是当图的顶点发生改变时,对于二维数组的修改会变得不太灵活。spa
图的另一种实现方式是邻接表,它是对邻接矩阵的一种改进。邻接表由图中每一个顶点的相邻顶点列表所组成。以下图所示,咱们能够用数组、链表、字典或散列表来表示邻接表。
咱们还能够用关联矩阵来表示图。在关联矩阵中,矩阵的行表示顶点,列表示边。关联矩阵一般用于边的数量比顶点多的状况下,以节省存储空间。以下图所示为关联矩阵方式表示的图:
下面咱们重点看下如何用邻接表的方式表示图。咱们的Graph类的骨架以下,它用邻接表方式来实现无向图:
class Graph { constructor () { this.vertices = []; // 用来存放图中的顶点 this.adjList = new Dictionary(); // 用来存放图中的边 } // 向图中添加一个新顶点 addVertex (v) {} // 向图中添加a和b两个顶点之间的边 addEdge (a, b) {} }
在Graph类中,咱们用数组vertices来保存图中的全部顶点,用字典(请参考《JavaScript数据结构——字典和散列表的实现》一文中的Dictionary类)adjList来保存图中每个顶点到相邻顶点的关系列表,在字典中,顶点被做为键值。请参考前面咱们给出的邻接表的示意图。而后在Graph类中,咱们提供两个方法,方法addVertex()用来向图中添加一个新顶点,方法addEdge()用来向图中添加给定的顶点a和顶点b之间的边。让咱们来看下这两个方法的实现。
addVertex (v) { if (!this.vertices.includes(v)) { this.vertices.push(v); this.adjList.set(v, []); } }
要添加一个新顶点,首先要判断该顶点在图中是否已经存在了,若是已经存在则不能添加。若是不存在,就在vertices数组中添加一个新元素,而后在字典adjList中添加一个以该顶点做为key的新元素,值为空数组。
addEdge (a, b) { // 若是图中没有顶点a,先添加顶点a if (!this.adjList.has(a)) { this.addVertex(a); } // 若是图中没有顶点b,先添加顶点b if (!this.adjList.has(b)) { this.addVertex(b); } this.adjList.get(a).push(b); // 在顶点a中添加指向顶点b的边 this.adjList.get(b).push(a); // 在顶点b中添加指向顶点a的边 }
addEdge()方法也很简单,首先要确保给定的两个顶点a和b在图中必须存在,若是不存在,则调用addVertex()方法进行添加,而后分别在字典中找到键值为顶点a和键值为顶点b的元素,在对应的值中添加一个新元素。
下面是Graph类的完整代码,其中的toString()方法是为了咱们测试用的,它的存在不是必须的。
1 class Graph { 2 constructor () { 3 this.vertices = []; // 用来存放图中的顶点 4 this.adjList = new Dictionary(); // 用来存放图中的边 5 } 6 7 // 向图中添加一个新顶点 8 addVertex (v) { 9 if (!this.vertices.includes(v)) { 10 this.vertices.push(v); 11 this.adjList.set(v, []); 12 } 13 } 14 15 // 向图中添加a和b两个顶点之间的边 16 addEdge (a, b) { 17 // 若是图中没有顶点a,先添加顶点a 18 if (!this.adjList.has(a)) { 19 this.addVertex(a); 20 } 21 // 若是图中没有顶点b,先添加顶点b 22 if (!this.adjList.has(b)) { 23 this.addVertex(b); 24 } 25 26 this.adjList.get(a).push(b); // 在顶点a中添加指向顶点b的边 27 this.adjList.get(b).push(a); // 在顶点b中添加指向顶点a的边 28 } 29 30 // 获取图的vertices 31 getVertices () { 32 return this.vertices; 33 } 34 35 // 获取图中的adjList 36 getAdjList () { 37 return this.adjList; 38 } 39 40 toString() { 41 let s = ''; 42 this.vertices.forEach((v) => { 43 s += `${v} -> `; 44 this.adjList.get(v).forEach((n) => { 45 s += `${n} `; 46 }); 47 s += '\n'; 48 }); 49 return s; 50 } 51 }
对于本文一开始给出的图,咱们添加下面的测试用例:
let graph = new Graph(); let myVertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']; myVertices.forEach((v) => { graph.addVertex(v); }); graph.addEdge('A', 'B'); graph.addEdge('A', 'C'); graph.addEdge('A', 'D'); graph.addEdge('C', 'D'); graph.addEdge('C', 'G'); graph.addEdge('D', 'G'); graph.addEdge('D', 'H'); graph.addEdge('B', 'E'); graph.addEdge('B', 'F'); graph.addEdge('E', 'I'); console.log(graph.toString());
下面是测试结果:
A -> B C D B -> A E F C -> A D G D -> A C G H E -> B I F -> B G -> C D H -> D I -> E
能够看到,与示意图是相符合的。
和树相似,咱们也能够对图进行遍历,以访问图中的全部顶点。图的遍历方式分为两种:广度优先(Breadth-First Search,BFS)和深度优先(Depth-First Search,DFS)。对图的遍历能够用来寻找特定的顶点或两个顶点之间的最短路径,以及检查图是否连通、图中是否含有环等。
算法 | 数据结构 | 描述 |
深度优先 | 栈 | 将图的顶点存入栈中(有关栈的介绍能够参考《JavaScript数据结构——栈的实现与应用》),顶点是沿着路径被探索的,存在新的相邻顶点就去访问。 |
广度优先 | 队列 | 将图的顶点存入队列中(有关队列的介绍能够参考《JavaScript数据结构——队列的实现与应用》),最早入队列的顶点先被探索。 |
在接下来要实现的算法中,咱们按照以下的约定对图中的顶点进行遍历,每一个顶点最多访问两次:
广度优先算法会从指定的第一个顶点开始遍历图,先访问这个顶点的全部相邻顶点,而后再访问这些相邻顶点的相邻顶点,以此类推。最终,广度优先算法会先广后深地访问图中的全部顶点。下面是广度优先遍历的示意图:
因为咱们采用邻接表的方式来存储图的数据,对于图的每一个顶点,都有一个字典与之对应,字典的键值为顶点的值,字典的内容为与该顶点相邻的顶点列表。基于这种数据结构,咱们能够考虑将全部顶点的邻接顶点存入队列,而后依次处理队列中的顶点。下面是具体的遍历步骤:
下面是该算法的具体实现:
let Colors = { WHITE: 0, GREY: 1, BLACK: 2 }; let initializeColor = vertices => { let color = {}; vertices.forEach(v => color[v] = Colors.WHITE); return color; }; let breadthFirstSearch = (graph, startVertex, callback) => { let vertices = graph.getVertices(); let adjList = graph.getAdjList(); let color = initializeColor(vertices); let queue = new Queue(); queue.enqueue(startVertex); while (!queue.isEmpty()) { let u = queue.dequeue(); adjList.get(u).forEach(n => { if (color[n] === Colors.WHITE) { color[n] = Colors.GREY; queue.enqueue(n); } }); color[u] = Colors.BLACK; if (callback) callback(u); } };
breadthFirstSearch()方法接收一个graph对象,图的数据经过该对象传入。参数startVertex指定了遍历的起始顶点。回调函数callback规定了要如何处理被遍历到的顶点。
首先经过initializeColor()函数将全部的顶点标记为未被访问过(颜色为白色),这些颜色保存在以顶点值为key的color对象中。图的vertices和adjList属性能够经过getVertices()和getAdjList()方法获得,而后构造一个队列queue(有关队列类Queue请参考《JavaScript数据结构——队列的实现与应用》),按照上面描述的步骤对图的顶点进行遍历。
在前面咱们给出的测试用例的基础上,添加下面的代码,来看看breadthFirstSearch()方法的执行结果:
breadthFirstSearch(graph, 'A', value => console.log(`visited vertex: ${value}`));
参数graph为前面测试用例中Graph类的实例,也就是咱们用来保存图的数据的对象,'A'被做为遍历的起始顶点,在回调函数中,打印一行文本,用来展现顶点被遍历的顺序。下面是测试结果:
visited vertex: A
visited vertex: B
visited vertex: C
visited vertex: D
visited vertex: E
visited vertex: F
visited vertex: G
visited vertex: H
visited vertex: I
尝试将'I'做为起始顶点,看看执行结果:
visited vertex: I
visited vertex: E
visited vertex: B
visited vertex: A
visited vertex: F
visited vertex: C
visited vertex: D
visited vertex: G
visited vertex: H
为了方便理解,咱们将顶点I放到最上面。从顶点I开始,首先遍历到的是它的相邻顶点E,而后是E的相邻顶点B,其次是B的相邻顶点A和F,A的相邻顶点C和D,C的相邻顶点G(D已经被遍历过了),最后是D的相邻顶点H(C和G已经被遍历过了)。
前面展现了广度优先算法的工做原理,咱们可使用它作更多的事情,例如在一个图G中,从顶点v开始到其它全部顶点间的最短距离。咱们考虑一下如何用BFS来实现寻找最短路径。
假设两个相邻顶点间的距离为1,从顶点v开始,在其路径上每通过一个顶点,距离加1。下面是对breadthFirstSearch()方法的改进,用来返回从起始顶点开始到其它全部顶点间的距离,以及全部顶点的前置顶点。
let BFS = (graph, startVertex) => { let vertices = graph.getVertices(); let adjList = graph.getAdjList(); let color = initializeColor(vertices); let queue = new Queue(); let distances = {}; let predecessors = {}; queue.enqueue(startVertex); // 初始化全部顶点的距离为0,前置节点为null vertices.forEach(v => { distances[v] = 0; predecessors[v] = null; }); while (!queue.isEmpty()) { let u = queue.dequeue(); adjList.get(u).forEach(n => { if (color[n] === Colors.WHITE) { color[n] = Colors.GREY; distances[n] = distances[u] + 1; predecessors[n] = u; queue.enqueue(n); } }); color[u] = Colors.BLACK; } return {distances, predecessors}; };
在BFS()方法中,咱们定义了两个对象distances和predecessors,用来保存从起始顶点出发到其它全部顶点的距离以及这些顶点的前置顶点。BFS()方法不须要callback回调函数,由于它会自行输出最终结果。与breadthFirstSearch()方法的逻辑相似,只不过在开始的时候将全部顶点的距离初始化为0,前置顶点初始化为null,而后在遍历的过程当中,从新设置顶点的distances值和predecessors值。咱们仍然将顶点A做为起始顶点,来看看测试结果:
console.log(BFS(graph, 'A'));
{ distances: { A: 0, B: 1, C: 1, D: 1, E: 2, F: 2, G: 2, H: 2, I: 3 }, predecessors: { A: null, B: 'A', C: 'A', D: 'A', E: 'B', F: 'B', G: 'C', H: 'D', I: 'E' } }
如你所见,distances为从顶点A开始到其它全部顶点的最短距离(相邻顶点间的距离为1),predecessors记录了全部顶点的前置顶点。以BFS()方法的返回结果为基础,经过下面的代码,咱们能够得出从顶点A开始到其它全部顶点的最短路径:
let shortestPathA = BFS(graph, 'A'); let startVertex = 'A'; myVertices.forEach(v => { let path = new Stack(); for (let v2 = v; v2 !== startVertex; v2 = shortestPathA.predecessors[v2]) { path.push(v2); } path.push(startVertex); let s = path.pop(); while (!path.isEmpty()) { s += ` - ${path.pop()}`; } console.log(s); });
其中的Stack类能够参考《JavaScript数据结构——栈的实现与应用》。下面是对应的执行结果:
A A - B A - C A - D A - B - E A - B - F A - C - G A - D - H A - B - E - I
以上咱们说的都是未加权的图,对于加权的图,广度优先算法并非最合适的。下面给出了另外几种最短路径算法:
Dijkstra - 寻找从指定顶点到其它全部顶点的最短路径的贪心算法。
1 const INF = Number.MAX_SAFE_INTEGER; 2 const minDistance = (dist, visited) => { 3 let min = INF; 4 let minIndex = -1; 5 for (let v = 0; v < dist.length; v++) { 6 if (visited[v] === false && dist[v] <= min) { 7 min = dist[v]; 8 minIndex = v; 9 } 10 } 11 return minIndex; 12 }; 13 const dijkstra = (graph, src) => { 14 const dist = []; 15 const visited = []; 16 const { length } = graph; 17 for (let i = 0; i < length; i++) { 18 dist[i] = INF; 19 visited[i] = false; 20 } 21 dist[src] = 0; 22 for (let i = 0; i < length - 1; i++) { 23 const u = minDistance(dist, visited); 24 visited[u] = true; 25 for (let v = 0; v < length; v++) { 26 if (!visited[v] && graph[u][v] !== 0 && dist[u] !== INF && dist[u] + graph[u][v] < dist[v]) { 27 dist[v] = dist[u] + graph[u][v]; 28 } 29 } 30 } 31 return dist; 32 };
Floyd-Warshall - 计算图中全部最短路径的动态规划算法。
1 const floydWarshall = graph => { 2 const dist = []; 3 const { length } = graph; 4 for (let i = 0; i < length; i++) { 5 dist[i] = []; 6 for (let j = 0; j < length; j++) { 7 if (i === j) { 8 dist[i][j] = 0; 9 } else if (!isFinite(graph[i][j])) { 10 dist[i][j] = Infinity; 11 } else { 12 dist[i][j] = graph[i][j]; 13 } 14 } 15 } 16 for (let k = 0; k < length; k++) { 17 for (let i = 0; i < length; i++) { 18 for (let j = 0; j < length; j++) { 19 if (dist[i][k] + dist[k][j] < dist[i][j]) { 20 dist[i][j] = dist[i][k] + dist[k][j]; 21 } 22 } 23 } 24 } 25 return dist; 26 };
Kruskal - 求解加权无向连通图的最小生成树(MST)的贪心算法。
1 const INF = Number.MAX_SAFE_INTEGER; 2 const find = (i, parent) => { 3 while (parent[i]) { 4 i = parent[i]; // eslint-disable-line prefer-destructuring 5 } 6 return i; 7 }; 8 const union = (i, j, parent) => { 9 if (i !== j) { 10 parent[j] = i; 11 return true; 12 } 13 return false; 14 }; 15 const initializeCost = graph => { 16 const cost = []; 17 const { length } = graph; 18 for (let i = 0; i < length; i++) { 19 cost[i] = []; 20 for (let j = 0; j < length; j++) { 21 if (graph[i][j] === 0) { 22 cost[i][j] = INF; 23 } else { 24 cost[i][j] = graph[i][j]; 25 } 26 } 27 } 28 return cost; 29 }; 30 const kruskal = graph => { 31 const { length } = graph; 32 const parent = []; 33 let ne = 0; 34 let a; 35 let b; 36 let u; 37 let v; 38 const cost = initializeCost(graph); 39 while (ne < length - 1) { 40 for (let i = 0, min = INF; i < length; i++) { 41 for (let j = 0; j < length; j++) { 42 if (cost[i][j] < min) { 43 min = cost[i][j]; 44 a = u = i; 45 b = v = j; 46 } 47 } 48 } 49 u = find(u, parent); 50 v = find(v, parent); 51 if (union(u, v, parent)) { 52 ne++; 53 } 54 cost[a][b] = cost[b][a] = INF; 55 } 56 return parent; 57 };
Prime - 求解加权无向连通图的最小生成树(MST)的贪心算法。
1 const INF = Number.MAX_SAFE_INTEGER; 2 const minKey = (graph, key, visited) => { 3 // Initialize min value 4 let min = INF; 5 let minIndex = 0; 6 for (let v = 0; v < graph.length; v++) { 7 if (visited[v] === false && key[v] < min) { 8 min = key[v]; 9 minIndex = v; 10 } 11 } 12 return minIndex; 13 }; 14 const prim = graph => { 15 const parent = []; 16 const key = []; 17 const visited = []; 18 const { length } = graph; 19 for (let i = 0; i < length; i++) { 20 key[i] = INF; 21 visited[i] = false; 22 } 23 key[0] = 0; 24 parent[0] = -1; 25 for (let i = 0; i < length - 1; i++) { 26 const u = minKey(graph, key, visited); 27 visited[u] = true; 28 for (let v = 0; v < length; v++) { 29 if (graph[u][v] && !visited[v] && graph[u][v] < key[v]) { 30 parent[v] = u; 31 key[v] = graph[u][v]; 32 } 33 } 34 } 35 return parent; 36 };
深度优先算法从图的第一个顶点开始,沿着这个顶点的一条路径递归查找到最后一个顶点,而后返回并探查路径上的其它路径,直到全部路径都被访问到。最终,深度优先算法会先深后广地访问图中的全部顶点。下面是深度优先遍历的示意图:
咱们仍然采用和广度优先算法同样的思路,一开始将全部的顶点初始化为白色,而后沿着路径递归探查其他顶点,当顶点被访问过,将颜色改成灰色,若是顶点被探索过(处理过),则将颜色改成黑色。下面是深度优先算法的具体实现:
let depthFirstSearchVisit = (u, color, adjList, callback) => { color[u] = Colors.GREY; if (callback) callback(u); adjList.get(u).forEach(n => { if (color[n] === Colors.WHITE) { depthFirstSearchVisit(n, color, adjList, callback); } }); color[u] = Colors.BLACK; }; let depthFirstSearch = (graph, callback) => { let vertices = graph.getVertices(); let adjList = graph.getAdjList(); let color = initializeColor(vertices); vertices.forEach(v => { if (color[v] === Colors.WHITE) { depthFirstSearchVisit(v, color, adjList, callback); } }); };
具体执行步骤为:
对应的测试用例及执行结果以下:
depthFirstSearch(graph, value => console.log(`visited vertex: ${value}`));
visited vertex: A
visited vertex: B
visited vertex: E
visited vertex: I
visited vertex: F
visited vertex: C
visited vertex: D
visited vertex: G
visited vertex: H
为了便于理解,咱们将整个遍历过程用下面的示意图来展现:
前面说过,深度优先算法的数据结构是栈,然而这里咱们并无使用栈来存储任何数据,而是使用了函数的递归调用,其实递归也是栈的一种表现形式。另一点,若是图是连通的(即图中任何两个顶点之间都存在路径),咱们能够对上述代码中的depthFirstSearch()方法进行改进,只须要对图的起始顶点开始遍历一次就能够了,而不须要遍历图的全部顶点,由于从起始顶点开始的递归就能够覆盖图的全部顶点。
前面展现了深度优先算法的工做原理,咱们可使用它作更多的事情,例如拓扑排序(toplogical sorting,也叫作topsort或者toposort)。与广度优先算法相似,咱们也对上面的depthFirstSeach()方法进行改进,以说明如何使用深度优先算法来实现拓扑排序:
let DFSVisit = (u, color, discovery, finished, predecessors, time, adjList) => { color[u] = Colors.GREY; discovery[u] = ++time.count; adjList.get(u).forEach(n => { if (color[n] === Colors.WHITE) { predecessors[n] = u; DFSVisit(n, color, discovery, finished, predecessors, time, adjList); } }); color[u] = Colors.BLACK; finished[u] = ++time.count; }; let DFS = graph => { let vertices = graph.getVertices(); let adjList = graph.getAdjList(); let color = initializeColor(vertices); let discovery = {}; let finished = {}; let predecessors = {}; let time = { count: 0 }; vertices.forEach(v => { finished[v] = 0; discovery[v] = 0; predecessors[v] = null; }); vertices.forEach(v => { if (color[v] === Colors.WHITE) { DFSVisit(v, color, discovery, finished, predecessors, time, adjList); } }); return {discovery, finished, predecessors}; };
DFS()方法会输出图中每一个顶点的发现时间和探索时间,咱们假定时间从0开始,每通过一步时间值加1。在DFS()方法中,咱们用变量discovery,finished,predecessors来保存每一个顶点的发现时间、探索时间和前置顶点(这个和广度优先算法中寻找最短路径中的一致,但最终执行结果会有区别),最终的输出结果中也会反映这三个值。这里须要注意的是,变量time之因此被定义为对象而不是一个普通的数字,是由于咱们须要在函数间传递这个变量,若是只是做为值传递,函数内部对变量的修改不会影响到它的原始值,可是咱们就是须要在函数递归调用的过程当中不断记录time的变化过程,因此采用值传递的方式显然不行。所以咱们将time定义为一个对象,对象被做为引用传递给函数,这样在函数内部对它的修改就会反映到原始值上。
来看看对DFS()方法的测试结果:
console.log(DFS(graph));
{ discovery: { A: 1, B: 2, C: 10, D: 11, E: 3, F: 7, G: 12, H: 14, I: 4 }, finished: { A: 18, B: 9, C: 17, D: 16, E: 6, F: 8, G: 13, H: 15, I: 5 }, predecessors: { A: null, B: 'A', C: 'A', D: 'C', E: 'B', F: 'B', G: 'D', H: 'D', I: 'E' } }
咱们将结果反映到示意图上,这样更加直观:
示意图上每个顶点左边的数字是顶点的发现时间,右边的数字是顶点的探索时间,所有完成时间是18,能够结合前面的深度优先算法遍历过程示意图来看,它们是对应的。同时咱们也看到,深度优先算法的predecessors和广度优先算法的predecessors会有所不一样。
拓扑排序只能应用于有向无环图(DAG)。基于上面DFS()方法的返回结果,咱们能够对顶点的完成时间(探索时间finished)进行排序,以获得咱们须要的拓扑排序结果。
若是要实现有向图,只须要对前面咱们实现的Graph类的addEdge()方法略加修改,将最后一行删掉。固然,咱们也能够在Graph类的构造函数中指明是有向图仍是无向图,下面是改进后的Graph类:
1 class Graph { 2 constructor (isDirected = false) { 3 this.isDirected = isDirected; 4 this.vertices = []; // 用来存放图中的顶点 5 this.adjList = new Dictionary(); // 用来存放图中的边 6 } 7 8 // 向图中添加一个新顶点 9 addVertex (v) { 10 if (!this.vertices.includes(v)) { 11 this.vertices.push(v); 12 this.adjList.set(v, []); 13 } 14 } 15 16 // 向图中添加a和b两个顶点之间的边 17 addEdge (a, b) { 18 // 若是图中没有顶点a,先添加顶点a 19 if (!this.adjList.has(a)) { 20 this.addVertex(a); 21 } 22 // 若是图中没有顶点b,先添加顶点b 23 if (!this.adjList.has(b)) { 24 this.addVertex(b); 25 } 26 27 this.adjList.get(a).push(b); // 在顶点a中添加指向顶点b的边 28 if (this.isDirected !== true) { 29 this.adjList.get(b).push(a); // 若是为无向图,则在顶点b中添加指向顶点a的边 30 } 31 } 32 33 // 获取图的vertices 34 getVertices () { 35 return this.vertices; 36 } 37 38 // 获取图中的adjList 39 getAdjList () { 40 return this.adjList; 41 } 42 43 toString() { 44 let s = ''; 45 this.vertices.forEach((v) => { 46 s += `${v} -> `; 47 this.adjList.get(v).forEach((n) => { 48 s += `${n} `; 49 }); 50 s += '\n'; 51 }); 52 return s; 53 } 54 }
而后咱们对有向图应用DFS算法:
let graph = new Graph(); let myVertices = ['A', 'B', 'C', 'D', 'E', 'F']; myVertices.forEach((v) => { graph.addVertex(v); }); graph.addEdge('A', 'C'); graph.addEdge('A', 'D'); graph.addEdge('B', 'D'); graph.addEdge('B', 'E'); graph.addEdge('C', 'F'); graph.addEdge('F', 'E'); console.log(DFS(graph));
下面是返回结果:
{ discovery: { A: 1, B: 11, C: 2, D: 8, E: 4, F: 3 }, finished: { A: 10, B: 12, C: 7, D: 9, E: 5, F: 6 }, predecessors: { A: null, B: null, C: 'A', D: 'A', E: 'F', F: 'C' } }
示意图以下:
对顶点的完成时间进行倒序排序,获得的拓扑排序结果为:B - A - D - C - F - E。
下一章咱们将介绍如何用JavaScript来实现各类常见的排序算法。