一、概述
图是计算机科学中的一类非常重要的数据结构,用于描述物体间的关系。在图中,节点(vertex)代表物体,边(edge)则代表物体间的关系。通过遍历图,我们可以对图中的节点进行访问,同时也可以找出图中节点之间的关系。
深度优先遍历是图遍历中重要的一种方法,它类似于二叉树的前序遍历,按照深度优先的方式访问图中的节点。具体而言,深度优先遍历会首先访问某个节点,接着访问该节点的邻居节点,并进一步访问邻居的邻居……直到访问到所有相关的节点。
本文主要介绍如何实现图的深度优先遍历,以及为什么它类似于二叉树。
二、图的深度优先遍历
在实现图的深度优先遍历时,我们需要考虑如何遍历每个节点,并标记哪些节点已经被遍历过。
下面是一个示例图,展示了深度优先遍历中的遍历顺序:
1 - 2 - 3 | | | 4 - 5 - 6
上图中,我们从节点1开始遍历,并首先访问它的邻居节点2。接着,我们访问节点2的邻居节点3,再访问节点3的邻居节点6……直到访问了所有相关的节点。
下面给出实现图的深度优先遍历的代码示例:
// 图的邻接表表示法 class Graph { constructor() { // 存储图的节点 this.vertices = []; // 存储图的边 this.edges = new Map(); } // 添加节点 addVertex(v) { this.vertices.push(v); this.edges.set(v, []); } // 添加边 addEdge(v, w) { this.edges.get(v).push(w); this.edges.get(w).push(v); } // 深度优先遍历 dfs(startingNode) { const visited = new Set(); this._dfs(startingNode, visited); } _dfs(vertex, visited) { visited.add(vertex); console.log(vertex); const neighbors = this.edges.get(vertex); for (let i = 0; i < neighbors.length; i++) { const neighbor = neighbors[i]; if (!visited.has(neighbor)) { this._dfs(neighbor, visited); } } } }
三、图的深度优先遍历类似于二叉树的前序遍历
虽然图和二叉树是两种不同的数据结构,但它们之间的遍历方式非常相似。图的深度优先遍历类似于二叉树的前序遍历,都是首先访问本节点,接着访问左子节点(或者邻居节点),最后访问右子节点(或者邻居节点)。下面是一个对比图和二叉树前序遍历的示意图:
图的深度优先遍历: 二叉树的前序遍历: 1 A / \ / \ 2 4 B D / \ \ / \ \ 3 5 6 C E F / \ / \ G H I J
从上面的示意图中可以看出,图的深度优先遍历和二叉树的前序遍历本质上是相同的遍历方式。唯一的区别在于,图没有一个指定的根节点,而是从任一节点开始遍历。
下面给出实现图的深度优先遍历类似于二叉树前序遍历的代码示例:
class Graph { // ... // 深度优先遍历类似于二叉树前序遍历 dfs(node) { const visited = new Set(); this._dfs(node, visited); } _dfs(node, visited) { visited.add(node); console.log(node); const neighbors = this.edges.get(node); for (const neighbor of neighbors) { if (!visited.has(neighbor)) { this._dfs(neighbor, visited); } } } }
四、图的深度优先遍历与回溯
在实现图的深度优先遍历时,我们需要考虑如何在访问之后回到原来的节点。相比于二叉树的遍历,图的遍历涉及到多个路径,因此需要更加小心地考虑回溯的问题。
在深度优先遍历过程中,我们使用递归函数来遍历每个节点。如果我们走到一个死路,即当前节点没有相邻节点可访问,那么我们需要回到之前的节点(回溯),查找还有没有相邻节点可访问。如果回到原来的节点后,还有未访问的相邻节点,那么我们将继续访问,直到访问完所有相关的节点。
下面给出实现遍历时的回溯代码示例:
class Graph { // ... // 深度优先遍历 dfs(startingNode) { const visited = new Set(); this._dfs(startingNode, visited); } _dfs(node, visited) { visited.add(node); console.log(node); const neighbors = this.edges.get(node); for (const neighbor of neighbors) { if (!visited.has(neighbor)) { this._dfs(neighbor, visited); } } } }
五、图的深度优先遍历与栈
在上面的示例代码中,我们使用递归实现图的深度优先遍历。但递归调用可能会导致栈溢出的问题,特别是在遍历大型图时,很容易出现栈溢出错误。因此,我们可以使用栈(stack)来实现深度优先遍历,而不是使用递归函数。
在使用栈实现深度优先遍历时,我们需要考虑如何在遍历完一个节点之后,回溯到它的前一个节点,并继续遍历。因此,我们需要维护一个栈,保存已经访问过的节点。同时,我们还需要维护一个表,记录每个节点是否已经被访问过。
下面给出使用栈实现遍历的代码示例:
class Graph { // ... iter_dfs(startingNode) { const visited = new Set(); const stack = []; stack.push(startingNode); while (stack.length !== 0) { const node = stack.pop(); if (!visited.has(node)) { visited.add(node); console.log(node); const neighbors = this.edges.get(node); for (const neighbor of neighbors) { if (!visited.has(neighbor)) { stack.push(neighbor); } } } } } }
六、总结
对于图的深度优先遍历类似于二叉树的前序遍历,我们需要遍历每个节点,并标记哪些节点已经被遍历过。在实现遍历时,我们需要考虑如何回溯到原来的节点,以及使用栈来替代递归实现深度优先遍历。
图的深度优先遍历类似于二叉树的前序遍历,但需要注意的是,图的遍历涉及到多个路径,因此需要更加小心地考虑回溯的问题。