有一个图,我们想访问它的所有顶点,就称为图的遍历。遍历图有两种方法:广度优先搜索和深度有优先搜索。
图遍历可以用来寻找特定的顶点或寻找两个顶点之间的路径,检查图是否连通。本文将详解图的两种遍历并用TypeScript将其实现,欢迎各位感兴趣的开发者阅读本文。
本文重点讲解图遍历的实现,对图和图两种遍历方式的概念不了解的开发者请移步我的另外几篇文章。
图的认识 | 深度优先搜索的理解与简单实现 | 广度优先搜索的理解与简单实现
图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于两种图遍历算法,都需要明确指出第一个被访问的顶点。
完全探索一个顶点,表示我们已经查看了该顶点的每一条边,对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问的顶点列表中。
为了保证算法的效率,务必访问每个顶点最多两次,连通图中每条边和顶点都会被访问到。
广度优先搜索算法和深度优先搜索算法基本上是相同的,唯一不同之处在于待访问顶点列表的数据结构。
我们用三种颜色描述各个顶点的访问状态。
我们需要实现一个辅助方法,用于初始化每个顶点的颜色。这个辅助方法实现也简单,参数传一个顶点列表,函数内部声明一个颜色对象,遍历顶点列表,将每个顶点的值作为颜色对象的key,颜色对象的value为白色。最后返回这个颜色对象。
接下来我们来分析下广度优先搜索如何实现。
广度优先搜索算法会从指定的一个顶点开始遍历图,先访问其所有的临点,一层一层的访问。
从一个顶点v开始进行广度优先搜索的实现思路如下:
上面我们分析了广度优先搜索的实现思路,我们将上述思路转换为代码。
/**
* 广度优先搜索
* @param graph 需要进行搜索的图
* @param startVertex 开始顶点
* @param callback 得到每个节点后的回调函数
*/
export const breadthFirstSearch = (graph: Graph, startVertex: string | number, callback: (val: string | number) => void): void => {
// 获取图的所有顶点
const vertices = graph.getVertices();
// 获取图的临接表
const adjList = graph.getAdjList();
// 将顶点进行初始化
const color = initializeColor(vertices);
// 实例化一个队列
const queue = new Queue();
// 将开始顶点入队
queue.enqueue(startVertex);
// 如果队列不为空就继续执行
while (!queue.isEmpty()) {
// 取出队列里存储的顶点u
const u = queue.dequeue();
// 获取取出顶点的临接表
const neighbors = <(string | number)[]>adjList.get(u);
// 将顶点列表里的u标识为已被访问但未被探索
color[u] = Colors.GERY;
// 遍历当前取出顶点的临接表
for (let i = 0; i < neighbors.length; i++) {
// 获取临接表中的每个顶点w
const w = neighbors[i];
// 如果w没被访问过
if (color[w] === Colors.WHITE) {
// 标识w为已被访问但未被探索
color[w] = Colors.GERY;
// 将w加入队列
queue.enqueue(w);
}
}
// 此时u顶点与其相邻顶点已经被探索,将u标识为已被访问且被完全探索
color[u] = Colors.BLACK;
// 执行回调函数
if (callback) {
callback(u);
}
}
};
完整代码请移步:Graph.ts
接下来,我们通过一个例子来测试下上述代码是否正确执行。
如下图所示,我们使用广度优先搜索访问图中的所有顶点。
let graph = new Graph();
let vertices = ["A", "B", "C", "D", "E", "F", "G", "H", "I"];
for (let i = 0; i < vertices.length; i++) {
graph.addVertex(vertices[i]);
}
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");
const printVertices = (val) => {
console.log(val);
};
breadthFirstSearch(graph, vertices[0], printVertices);
上面我们学习了广度优先搜索的基本原理,我们还可以用该算法做更多的事情,而不只是输出被访问节点的顺序。
例如,给定一个图G和源顶点v,找出每个顶点u和v之间的最短路径的距离(以边的数量计)
对于给定顶点v,广度优先算法会访问所有与其距离为1的顶点,接着是距离为2的顶点,以此类推。所以,可以用广度优先算法来解决这个问题。
我们修改上面实现的广度优先算法,让其返回如下信息:
接下来我们来分析下如何修改算法来返回我们需要的信息。
代码实现如下:
/**
* 广度优先搜索优化
* @param graph 要进行遍历的图
* @param startVertex 开始顶点
* @constructor
*/
export const BFS = (
graph: Graph,
startVertex: string | number
): { distances: { [key: string]: string | number }; predecessors: { [key: string]: string | number | null } } => {
// 获取图的所有顶点
const vertices = <(string | number)[]>graph.getVertices();
// 获取图的临接表
const adjList = graph.getAdjList();
// 初始化顶点颜色
const color = initializeColor(vertices);
// 创建一个队列
const queue = new Queue();
// 存储每个顶点的距离
const distances: { [key: string]: string | number } = {};
// 存储前溯点
const predecessors: { [key: string]: string | null | number } = {};
// 顶点入队
queue.enqueue(startVertex);
// 遍历所有顶点
for (let i = 0; i < vertices.length; i++) {
// 用0来初始化每个顶点的距离
distances[vertices[i]] = 0;
// 用null来初始化每个顶点的前溯点
predecessors[vertices[i]] = null;
}
while (!queue.isEmpty()) {
// 获取队首顶点u
const u = queue.dequeue();
// 获取u的临接表
const neighbors = <(string | number)[]>adjList.get(u);
// u标识为已访问但未被探索状态
color[u] = Colors.GERY;
// 遍历u的临接表
for (let i = 0; i < neighbors.length; i++) {
// 获取临接表中遍历到的顶点w
const w = neighbors[i];
// 如果顶点w未被访问
if (color[w] === Colors.WHITE) {
// 标识顶点w为已访问但为被探索
color[w] = Colors.GERY;
// 给u顶点加1来增加v和w之间的距离(u是w的前溯点)
distances[w] = <number>distances[u] + 1;
// 发现顶点u的邻点w时,则设置w的前溯点值为u
predecessors[w] = u;
// w入栈
queue.enqueue(w);
}
}
}
return {
distances,
predecessors
};
};
接下来我们测试下,优化后的方法是否正常执行。
const shortestPaths = BFS(graph, vertices[0]);
console.log(shortestPaths);
执行上述代码后,我们会得到如下图所示的结果。
解释如下:
# 距离
distances: { A: 0, B: 1, C: 1, D: 1, E: 2, F: 2, G: 2, H: 2, I: 3 }
顶点A到A的距离为0
顶点A到B的距离为1
顶点A到C的距离为1
顶点A到D的距离为1
顶点A到E的距离为2
顶点A到F的距离为2
顶点A到G的距离为2
顶点A到I的距离为3
# 前溯点
predecessors: { A: null, B: "A", C: "A", D: "A", E: "B", F: "B", G: "C", H: "D", I: "E" }
顶点A的前溯点为null
顶点B的前溯点为A
顶点C的前溯点为A
顶点D的前溯点为A
顶点E的前溯点为B
顶点F的前溯点为为B
顶点G的前溯点为C
顶点H的前溯点为D
顶点I的前溯点为E
上面我们拿到了A到每个顶点距离和每个顶点的前溯点,接下来我们就可以根据距离和前溯点来求最短距离了。
/**
通过前溯点列表获取顶点A到其他顶点的路径
*/
// 用顶点A作为源顶点
const fromVertex = vertices[0];
// 遍历除过源顶点外的顶点
for (let i = 1; i < vertices.length; i++) {
// 获取A抵达的顶点
const toVertex = vertices[i];
// 创建一个栈来存储路径值
const path = new Stack();
// 追溯toVertex到fromVertex的路径,变量v赋值为其前溯点的值
for (let v = toVertex; v !== fromVertex; v = shortestPaths.predecessors[v]) {
// v入栈
path.push(v);
}
// 源顶点入栈
path.push(fromVertex);
let s = path.pop();
while (!path.isEmpty()) {
s += " - " + path.pop();
}
console.log(s);
}
完整代码请移步: GraphTest.js
执行结果如下
深度优先搜索算法将会从一个指定的顶点开始遍历图,沿着路径查找,直至这条这条路径的最后一个顶点被访问,接着原路回退并探索下一条路径。如下图所示
深度优先搜索不需要一个源顶点,在深度优先算法中,若图中顶点v未访问,则访问该顶点v。
要访问顶点v,实现思路如下。
递归访问顶点的实现思路如下。
接下来我们将上述思路转换为代码。
/**
* 深度优先搜索
* @param graph 要进行遍历的图
* @param callback 回调函数
*/
export const depthFirstSearch = (graph: Graph, callback: (val: string | number) => void): void => {
// 获取图的顶点
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);
}
}
};
/**
* 递归访问顶点
* @param u 要访问的顶点
* @param color 颜色对象
* @param adjList 图的临接表
* @param callback 回调函数
*/
const depthFirstSearchVisit = (
u: string | number,
color: { [p: string]: number },
adjList: Dictionary<string | number, (string | number)[]>,
callback: (val: string | number) => void
) => {
// 顶点u访问后,标识为已访问但未被探索状态
color[u] = Colors.GERY;
// 执行回调函数
if (callback) {
callback(u);
}
// 获取当前顶点的临接表
const neighbors = <string | number[]>adjList.get(u);
// 遍历临接表
for (let i = 0; i < neighbors.length; i++) {
// 获取临接表的每个顶点w
const w = neighbors[i];
// 如果w未被访问则以w为顶点进行递归访问
if (color[w] === Colors.WHITE) {
depthFirstSearchVisit(w, color, adjList, callback);
}
}
// u标识为已被完全探索
color[u] = Colors.BLACK;
};
完整代码请移步:Graph.ts
接下来我们用广度优先搜索的例子来测试下上述代码是否都正确执行。
console.log("深度优先搜索节点访问顺序");
depthFirstSearch(graph, printVertices);
上面我们学习了深度优先搜索的基本原理,我们可以用深度优先搜索做很多事情,而不只是输出顶点的被访问顺序。
例如,给定一个图G,我们希望深度优先算法遍历图G的所有顶点,构建“森林”以及一组源顶点,并输出两个数组:发现时间和完成探索时间。
我们修改深度优先搜索算法,让其实现返回以下信息。
接下来我们分析下,如果通过修改代码让其返回我们需要的信息。
/**
* 优化后的深度优先搜索
* @param graph 要进行搜索的图
* @constructor
*/
export const DFS = (
graph: Graph
): { predecessors: { [key: string]: string | number | null }; discovery: { [key: string]: string | number }; finished: { [key: string]: string | number } } => {
const vertices = <(number | string)[]>graph.getVertices();
const adjList = graph.getAdjList();
const color = initializeColor(vertices);
const d: { [key: string]: string | number } = {};
const f: { [key: string]: string | number } = {};
const p: { [key: string]: string | number | null } = {};
const time: { [key: string]: number } = { 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) {
// 从i顶点开始递归访问
DFSVisit(vertices[i], color, d, f, p, time, adjList);
}
}
return {
discovery: d,
finished: f,
predecessors: p
};
};
/**
* 递归访问顶点
* @param u 要访问的顶点
* @param color 颜色对象
* @param d 顶点初次发现时间
* @param f 顶点完成访问时间
* @param p 前溯点
* @param time 初始时间
* @param adjList 临接表
* @constructor
*/
const DFSVisit = (
u: string | number,
color: { [p: string]: number },
d: { [key: string]: string | number },
f: { [key: string]: string | number },
p: { [key: string]: string | number | null },
time: { [key: string]: number },
adjList: Dictionary<string | number, (string | number)[]>
) => {
// 顶点u被发现但未被探索
color[u] = Colors.GERY;
// 记录顶点u的初次发现时间
d[u] = ++time["count"];
// 获取顶点u的临接表
const neighbors = <(string | number)[]>adjList.get(u);
for (let i = 0; i < neighbors.length; i++) {
// 获取w临接点
const w = neighbors[i];
// 如果w未被访问,存储w的前溯点,以w未顶点继续递归访问
if (color[w] === Colors.WHITE) {
p[w] = u;
DFSVisit(w, color, d, f, p, time, adjList);
}
}
// 顶点被完全访问
color[u] = Colors.BLACK;
// 记录顶点完成访问时间
f[u] = ++time["count"];
};
接下来我们测试下优化后的方法是否能正常执行。
console.log("优化后的深度优先搜索");
console.log(DFS(graph));
执行上述代码后,我们会得到如下图所示的结果。
解释如下
顶点A发现时间是1,完成访问的时间是18
顶点B的发现时间是2,完成访问的时间是9
顶点C的发现时间是10,完成访问的时间17
顶点D的发现时间是11,完成访问的时间是16
顶点E的发现时间是3,完成访问的时间6
顶点F的发现时间是7,完成访问的时间8
...
顶点I的发现时间是4,完成访问的时间是5
上述执行结果也可以用下图来描述
我们通过一个例子来讲解拓扑排序,如下图所示,假定每个顶点都是一个需要去执行的任务。
上图是一个有向图,意味着任务的执行是有顺序的。例如任务F不能在任务A之前执行,这个图没有环,意味着这张图是一个无环图,因此我们可以称上图为有向无环图。
当我们优化深度优先搜索算法后,拿到了我们需要的数据,就可以根据这些数据,稍作调整就能完成拓扑排序了。
// 实现拓扑排序
graph = new Graph(true);
vertices = ["A", "B", "C", "D", "E", "F"];
for (let i = 0; i < vertices.length; i++) {
graph.addVertex(vertices[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);
console.log("拓扑排序");
const fTimes = result.finished;
let s = "";
for (let count = 0; count < vertices.length; count++) {
let max = 0;
let maxName = null;
for (let i = 0; i < vertices.length; i++) {
if (fTimes[vertices[i]] > max) {
max = fTimes[vertices[i]];
maxName = vertices[i];
}
}
s += maxName + " - ";
delete fTimes[maxName];
}
console.log(s);
完整代码请移步: GraphTest.js
上述代码的执行结果如下图所示
阅读量:2016
点赞量:0
收藏量:0