算法|图的遍历-深度优先搜索(DFS)
什么是深度优先搜索(DFS)
前一篇描述了,搜索图的算法之一的广度优先搜索(breadth-first search,BFS),而另外一种搜索图的算法,则是深度优先搜索(depth-first search,DFS)。
深度优先搜索算法(DFS)会从第一个指定的顶点开始遍历图,沿着路径直到这条路径的最后一个顶点被访问了,再原路折回探索吓一跳路径。
深度优先搜索是"先深度后广度地访问顶点"的图搜索算法。
深度优先搜索(DFS)全过程
例如用下面的图结构,从顶点A开始搜索,直到搜索到顶点G。
可以知道,顶点A的相邻顶点有顶点B、顶点C、顶点D,这三个顶点,将它们视为下一步的候选顶点,按顺序逐个深度递归搜索。由于顶点B是最先探索到的相邻顶点,则从顶点B开始深度探索,移动到顶点B。
到达顶点B,可以获知它的相邻顶点是E和F,则将它们视为下一步的候选顶点,由于顶点E是最先探索到的相邻顶点,则继续从顶点E开始深度探索,移动到顶点E。
到达顶点E,可以获知它的相邻顶点是K,则将它视为下一步的候选顶点,继续从顶点K开始深度探索,移动到顶点K。
到达顶点K,由于顶点K没有路径下的相邻顶点,则原路折返知道有未探索顶点的顶点B,探索顶点F。
接下来不断重复相同的操作:
•到达顶点F,由于顶点F没有路径下的相邻顶点,则原路折返知道有未探索顶点的顶点A,探索顶点C。•到达顶点C,可以获知它的相邻顶点是H,则将它视为下一步的候选顶点,继续从顶点H开始深度探索,移动到顶点H。•到达顶点C,可以获知它的相邻顶点是H,则将它视为下一步的候选顶点,继续从顶点H开始深度探索,移动到顶点H。•到达顶点H,可以获知它的相邻顶点是G,则将它视为下一步的候选顶点,继续从顶点G开始深度探索,移动到顶点G。•到达顶点G,找到需要到达的目标,结束搜索。
实现深度优先搜索(DFS)
代码流程
•为了标记图的顶点,我们需要提供一个表示顶点颜色的常量对象。•白色(WHITE):表示该顶点还没有被访问•灰色(GREY):表示该顶点被访问过,但并未被探索过•黑色(BLACK):表示该顶点被访问过且被完全探索过
// 广度优先算法中标记顶点的常量对象
const Colors = {
WHITE: 0, // 表示该顶点还没有被访问
GREY: 1, // 表示该顶点被访问过,但并未被探索过
BLACK: 2 // 表示该顶点被访问过且被完全探索过
};
•并且提供一个辅助方法,用于初始化每个顶点颜色。
/**
* 辅助方法:初始化每个顶点的颜色的函数
* @param {*} vertices 顶点列表
* @returns {*} 返回初始化顶点颜色后的对象
*/
const initializeColor = vertices => {
const color = {};
for (let i = 0; i < vertices.length; i++) {
color[vertices[i]] = Colors.WHITE;
}
return color;
};
接着便是深度优先搜索算法的完整代码思路:
•声明广度优先搜索算法的函数-depthFirstSearch,接收两个参数,分别是graph(要遍历的图实例)和callback(回调函数)•在函数内获取传入图实例(graph)的顶点列表、邻接表,并将图的顶点列表中的所有顶点初始化颜色为白色(表示该顶点还没有被访问)•遍历图实例的所有顶点,如果当前遍历的顶点项是白色(表示该顶点还没有被访问),则递归访问这个顶点,递归顶点使用的函数-depthFirstSearchVisit,传入函数需要的四个参数要访问的顶点(u)、图所有顶点的颜色对象(color)、图的邻接表(adjList)和回调函数(callback),它内部的代码流程:•把当前访问的顶点项(u),标识为灰色(该顶点被访问过,但并未被探索过)•如果有传入回调函数,则执行该回调函数,回调函数会将这个顶点作为入参方法。(执行该函数输出已访问过的顶点)•获取当前访问顶点项(u)的邻接表,如果这个顶点的邻接表不为空,则遍历这个顶点的邻接表中的顶点,遍历时,如果该邻接表的顶点是白色,则以该邻接表的顶点,进行递归访问。•这个顶点的邻接表探索完后,将这个顶点标识为黑色(已被访问且被完全探索)
代码实现
/**
* 深度优先搜索顶点(私有函数)
* @param {*} u 要访问的顶点
* @param {object} color 图所有顶点的颜色对象
* @param {*} adjList 图的邻接表
* @param {function} callback 回调函数
*/
const depthFirstSearchVisit = (u, color, adjList, callback) => {
color[u] = Colors.GREY; // 当前访问的顶点u标识为灰色(该顶点被访问过,但并未被探索过)
// 如果存在回调函数
if (callback) {
callback(u); // 则指向回调函数,入参顶点u(执行该函数输出已访问过的顶点)
}
// console.log('Discovered ' + u);
const neighbors = adjList.get(u); // 获取顶点u的邻接表
for (let i = 0; i < neighbors.length; i++) { // 循环遍历顶点u的邻接表
const w = neighbors[i]; // 取出邻接表的每个顶点,赋值给w
if (color[w] === Colors.WHITE) { // 如果当前顶点w是白色(顶点还没有被访问),则以w为顶点进行递归访问
depthFirstSearchVisit(w, color, adjList, callback);
}
}
color[u] = Colors.BLACK; // 最后标识u为黑色(该顶点被访问过且被完全探索过)
// console.log('explored ' + u);
};
/**
* 图的深度优先搜索算法
* 使用到的数据结构:堆栈
* 概念:先深度后广度地访问顶点
* @param {*} graph 要进行遍历的图
* @param {function} callback 回调函数
*/
export 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); // 则调用私有的递归函数搜索顶点
}
}
};
代码用例
我们初始化一个图,用于测试深度优先搜索。
let graph = new Graph();
const myVertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
for (let i = 0; i < myVertices.length; i++) {
graph.addVertex(myVertices[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 printVertex = (value) => console.log('Visited vertex: ' + value);
depthFirstSearch(graph, printVertex);
/**
*
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
*/
深度优先搜索算法(DFS)还可以做更多
深度优先搜索算法的工作原理了解之后,它还可以做更多事情,比如使用它来搜索图顶点的初次发现时间、完成访问时间,或者图中某个顶点的前溯点。
下面是一段利用DFS探索图实例中每个顶点的初次发现时间、完成访问时间和前溯点。
代码实现
/**
* 深度探索顶点
* @param u 要访问的顶点
* @param color 颜色对象
* @param d 图中每个顶点的初次发现时间
* @param f 图中每个顶点的完成访问时间
* @param p 图中每个顶点的前溯点
* @param time 初始时间
* @param adjList 图的临接表
* @constructor
*/
const DFSVisit = (u, color, d, f, p, time, adjList) => {
// console.log('discovered ' + u);
color[u] = Colors.GREY; // 当前访问的顶点u标识为灰色(该顶点被访问过,但并未被探索过)
d[u] = ++time.count; // 递增(追加)当前顶点的发现时间
const neighbors = adjList.get(u); // 获取顶点u的邻接表
for (let i = 0; i < neighbors.length; i++) { // 循环遍历顶点u的邻接表
const w = neighbors[i]; // 取出邻接表的每个顶点,赋值给w
if (color[w] === Colors.WHITE) { // 如果当前顶点w是白色(顶点还没有被访问),则以w为顶点进行递归访问
p[w] = u; // 当顶点w是由引自顶点u的边而被发现的,设置顶点w的前溯点为顶点u
DFSVisit(w, color, d, f, p, time, adjList); // 以w为顶点进行递归访问
}
}
color[u] = Colors.BLACK; // 最后标识u为黑色(该顶点被访问过且被完全探索过)
f[u] = ++time.count; //递增(追加)顶点u的发现时间,并记录在顶点u的完成访问时间中
// console.log('explored ' + u);
};
/**
* 使用深度优先算法探索图
* @param {*} graph 要进行搜索的图(Graph 类实例)
*/
export const DFS = graph => {
const vertices = graph.getVertices(); // 获取传入参数的图,它的顶点列表
const adjList = graph.getAdjList(); // 获取传入参数的图,它的邻接表
const color = initializeColor(vertices); // 将图的顶点列表中的所有顶点初始化为白色
const d = {}; // 存储每个顶点的发现时间(distances)
const f = {}; // 存储每个顶点完成访问的时间(distances)
const p = {}; // 存储前溯点(predecessors)
const time = {
count: 0
}; // time来追踪发现时间和完成探索时间
for (let i = 0; i < vertices.length; i++) { // 遍历图的顶点列表,完成初始化
f[vertices[i]] = 0; // 初始化图中每一个顶点的完成访问时间为0
d[vertices[i]] = 0; // 初始化图中每一个顶点的发现时间为0
p[vertices[i]] = null; // 初始化图中每一个顶点的前溯点为null
}
for (let i = 0; i < vertices.length; i++) { // 再次遍历图的顶点列表,完成初始化
if (color[vertices[i]] === Colors.WHITE) { // 如果遍历的当前项顶点是白色(该顶点还没有被访问)
DFSVisit(vertices[i], color, d, f, p, time, adjList); // 则调用私有的递归函数深度探索顶点
}
}
// 最后,返回distances对象、finished对象和predecessors对象
return {
discovery: d,
finished: f,
predecessors: p
};
};
使用DFS实现拓扑排序
什么是拓扑排序
当我需要编排一些任务或步骤的执行顺序时,这称为拓扑排序(topological sorting,英文亦写作 topsort 或是 toposort)。
例如,当开始学习一门计算机科学课程,在学习某些知识之前得按顺序完成一些知识储备(你不可以在上 算法 I 课程前先上算法 II 课程)。
当我们在开发一个项目时,需要按顺序执行一些步骤。例如,首先从客户那里得到需求,接着开发客户要求的东西,最后交付项目。你不能先交付项目再去收集需求。
代码用例
/**
* 使用DFS实现拓扑排序
*/
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);
/**
distances: { 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: "B", F: "C"]
顶点A的发现时间为1,完成访问时间是10,前溯点为null
顶点B的发现时间为11,完成访问时间是12,前溯点为null
顶点C的发现时间为2,完成访问时间是7,前溯点为A
顶点D的发现时间为8,完成访问时间是9,前溯点为A
顶点E的发现时间为4,完成访问时间是5,前溯点为B
顶点F的发现时间为3,完成访问时间是6,前溯点为C
*/
const fTimes = result.finished; // 图的每个顶点的完成访问的时间
let s = ''; // 用于输出排序的字符串
for (let count = 0; count < myVertices.length; count++) { // 遍历图的顶点列表
let max = 0; // 初始化max变量存储最大的顶点完成访问时间
let maxName = null; // 初始化maxName变量存储完成访问时间最大的顶点
for (i = 0; i < myVertices.length; i++) { // 再次遍历图的顶点列表
if (fTimes[myVertices[i]] > max) { // 如果当前顶点的完成访问时间大于max
max = fTimes[myVertices[i]]; // 设置max为当前顶点的完成访问时间
maxName = myVertices[i]; // 设置maxName为当前顶点
}
}
s += ' - ' + maxName; // 向字符串追加顶点
delete fTimes[maxName]; // 从图的完成访问时间对象中删除当前顶点
}
console.log(s);
/**
* 最终输出结果(依据图顶点的完成访问时间大小倒序排列)
* B - A - D - C - F - E
*/
完整代码实现
附上上述所有代码的完整实现
import {
Queue
} from '/Queue/app.js';
import {
Graph
} from '/Graph/app.js';
/**
* 有两种算法可以对图进行遍历:广度优先搜索(breadth-first search,BFS)和深度优先搜索(depth-first search,DFS)。图遍历可以用来寻找特定的顶点或寻找两个顶点之间的路径,检查图是否连通,检查图是否含有环,等等。
*/
// 深度优先算法中标记顶点的常量对象
const Colors = {
WHITE: 0, // 表示该顶点还没有被访问
GREY: 1, // 表示该顶点被访问过,但并未被探索过
BLACK: 2 // 表示该顶点被访问过且被完全探索过
};
/**
* 辅助方法:初始化每个顶点的颜色的函数
* @param {*} vertices 顶点列表
* @returns {*} 返回初始化顶点颜色后的对象
*/
const initializeColor = vertices => {
const color = {};
for (let i = 0; i < vertices.length; i++) {
color[vertices[i]] = Colors.WHITE;
}
return color;
};
/**
* 深度优先搜索顶点(私有函数)
* @param {*} u 要访问的顶点
* @param {object} color 图所有顶点的颜色对象
* @param {*} adjList 图的邻接表
* @param {function} callback 回调函数
*/
const depthFirstSearchVisit = (u, color, adjList, callback) => {
color[u] = Colors.GREY; // 当前访问的顶点u标识为灰色(该顶点被访问过,但并未被探索过)
// 如果存在回调函数
if (callback) {
callback(u); // 则指向回调函数,入参顶点u(执行该函数输出已访问过的顶点)
}
// console.log('Discovered ' + u);
const neighbors = adjList.get(u); // 获取顶点u的邻接表
for (let i = 0; i < neighbors.length; i++) { // 循环遍历顶点u的邻接表
const w = neighbors[i]; // 取出邻接表的每个顶点,赋值给w
if (color[w] === Colors.WHITE) { // 如果当前顶点w是白色(顶点还没有被访问),则以w为顶点进行递归访问
depthFirstSearchVisit(w, color, adjList, callback);
}
}
color[u] = Colors.BLACK; // 最后标识u为黑色(该顶点被访问过且被完全探索过)
// console.log('explored ' + u);
};
/**
* 图的深度优先搜索算法
* 使用到的数据结构:堆栈
* 概念:先深度后广度地访问顶点
* @param {*} graph 要进行遍历的图
* @param {function} callback 回调函数
*/
export 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); // 则调用私有的递归函数搜索顶点
}
}
};
let graph = new Graph();
const myVertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
for (let i = 0; i < myVertices.length; i++) {
graph.addVertex(myVertices[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 printVertex = (value) => console.log('Visited vertex: ' + value);
depthFirstSearch(graph, printVertex);
/**
*
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
*/
/**
* 深度探索顶点
* @param u 要访问的顶点
* @param color 颜色对象
* @param d 图中每个顶点的初次发现时间
* @param f 图中每个顶点的完成访问时间
* @param p 图中每个顶点的前溯点
* @param time 初始时间
* @param adjList 图的临接表
* @constructor
*/
const DFSVisit = (u, color, d, f, p, time, adjList) => {
// console.log('discovered ' + u);
color[u] = Colors.GREY; // 当前访问的顶点u标识为灰色(该顶点被访问过,但并未被探索过)
d[u] = ++time.count; // 递增(追加)当前顶点的发现时间
const neighbors = adjList.get(u); // 获取顶点u的邻接表
for (let i = 0; i < neighbors.length; i++) { // 循环遍历顶点u的邻接表
const w = neighbors[i]; // 取出邻接表的每个顶点,赋值给w
if (color[w] === Colors.WHITE) { // 如果当前顶点w是白色(顶点还没有被访问),则以w为顶点进行递归访问
p[w] = u; // 当顶点w是由引自顶点u的边而被发现的,设置顶点w的前溯点为顶点u
DFSVisit(w, color, d, f, p, time, adjList); // 以w为顶点进行递归访问
}
}
color[u] = Colors.BLACK; // 最后标识u为黑色(该顶点被访问过且被完全探索过)
f[u] = ++time.count; //递增(追加)顶点u的发现时间,并记录在顶点u的完成访问时间中
// console.log('explored ' + u);
};
/**
* 使用深度优先算法探索图
* @param {*} graph 要进行搜索的图(Graph 类实例)
*/
export const DFS = graph => {
const vertices = graph.getVertices(); // 获取传入参数的图,它的顶点列表
const adjList = graph.getAdjList(); // 获取传入参数的图,它的邻接表
const color = initializeColor(vertices); // 将图的顶点列表中的所有顶点初始化为白色
const d = {}; // 存储每个顶点的发现时间(distances)
const f = {}; // 存储每个顶点完成访问的时间(distances)
const p = {}; // 存储前溯点(predecessors)
const time = {
count: 0
}; // time来追踪发现时间和完成探索时间
for (let i = 0; i < vertices.length; i++) { // 遍历图的顶点列表,完成初始化
f[vertices[i]] = 0; // 初始化图中每一个顶点的完成访问时间为0
d[vertices[i]] = 0; // 初始化图中每一个顶点的发现时间为0
p[vertices[i]] = null; // 初始化图中每一个顶点的前溯点为null
}
for (let i = 0; i < vertices.length; i++) { // 再次遍历图的顶点列表,完成初始化
if (color[vertices[i]] === Colors.WHITE) { // 如果遍历的当前项顶点是白色(该顶点还没有被访问)
DFSVisit(vertices[i], color, d, f, p, time, adjList); // 则调用私有的递归函数深度探索顶点
}
}
// 最后,返回distances对象、finished对象和predecessors对象
return {
discovery: d,
finished: f,
predecessors: p
};
};
/**
* 使用DFS实现拓扑排序
*/
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);
/**
distances: { 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: "B", F: "C"]
顶点A的发现时间为1,完成访问时间是10,前溯点为null
顶点B的发现时间为11,完成访问时间是12,前溯点为null
顶点C的发现时间为2,完成访问时间是7,前溯点为A
顶点D的发现时间为8,完成访问时间是9,前溯点为A
顶点E的发现时间为4,完成访问时间是5,前溯点为B
顶点F的发现时间为3,完成访问时间是6,前溯点为C
*/
const fTimes = result.finished; // 图的每个顶点的完成访问的时间
let s = ''; // 用于输出排序的字符串
for (let count = 0; count < myVertices.length; count++) { // 遍历图的顶点列表
let max = 0; // 初始化max变量存储最大的顶点完成访问时间
let maxName = null; // 初始化maxName变量存储完成访问时间最大的顶点
for (i = 0; i < myVertices.length; i++) { // 再次遍历图的顶点列表
if (fTimes[myVertices[i]] > max) { // 如果当前顶点的完成访问时间大于max
max = fTimes[myVertices[i]]; // 设置max为当前顶点的完成访问时间
maxName = myVertices[i]; // 设置maxName为当前顶点
}
}
s += ' - ' + maxName; // 向字符串追加顶点
delete fTimes[maxName]; // 从图的完成访问时间对象中删除当前顶点
}
console.log(s);
/**
* 最终输出结果(依据图顶点的完成访问时间大小倒序排列)
* B - A - D - C - F - E
*/