深度优先搜索(DFS)算法详解
1. 深度优先搜索(DFS)的基本概念
DFS 是一种用于图(Graph)和树(Tree)数据结构中的遍历算法。它尽可能地沿着一个分支深入下去,直到遇到没有未访问的邻接节点时回溯,继续探索其他分支,直到所有节点都被访问。
DFS 的实现通常基于以下两种方式之一:
- 递归(利用函数调用栈隐式地进行回溯)
- 显式栈(使用自定义的栈来模拟递归)
2. DFS 的工作原理
- 从图的起始节点开始访问。
- 标记当前节点为已访问。
- 遍历当前节点的所有邻居,对于每个邻居,如果尚未访问,递归地进行深度优先搜索。
- 如果该节点的所有邻居都已访问,递归结束,自动回溯到上一个节点,继续搜索其他分支。
3. DFS 的应用场景
- 连通性检测:检查图中是否存在连通分量。
- 拓扑排序:在有向无环图(DAG)中,利用 DFS 来确定节点的线性排序。
- 路径查找:用于找到从起点到目标点的路径。
- 图的强连通分量:通过 DFS 可以找到图的强连通分量。
4. DFS 的复杂度分析
- 时间复杂度:O(V + E),其中 V 是节点数,E 是边数。每个节点和每条边都最多被访问一次。
- 空间复杂度:O(V),递归调用栈的深度为 V,另外还需要存储访问标记。
5. DFS 的实现
(1) 递归实现
递归实现利用函数调用栈来隐式地进行回溯。以下代码示例展示了递归实现的 DFS:
#include <iostream>
#include <vector>
using namespace std;
void DFSUtil(int iNode, const vector<vector<int>>& vvGraph, vector<bool>& vbVisited)
{
// 标记当前节点为已访问
vbVisited[iNode] = true;
cout << iNode << " ";
// 遍历当前节点的所有邻居节点
for(int iNeighbor : vvGraph[iNode])
{
if(!vbVisited[iNeighbor])
{
// 递归调用DFS访问邻居
DFSUtil(iNeighbor, vvGraph, vbVisited);
}
}
}
void DFS(const vector<vector<int>>& vvGraph, int iStartNode)
{
int iTotalNodes = vvGraph.size();
// 创建访问标记数组
vector<bool> vbVisited(iTotalNodes, false);
// 从起始节点执行DFS
DFSUtil(iStartNode, vvGraph, vbVisited);
}
int main()
{
// 创建图的邻接表
vector<vector<int>> vvGraph = {
{1, 2}, // 节点 0 与节点 1、2 相连
{0, 2, 3}, // 节点 1 与节点 0、2、3 相连
{0, 1, 4}, // 节点 2 与节点 0、1、4 相连
{1, 5}, // 节点 3 与节点 1、5 相连
{2}, // 节点 4 与节点 2 相连
{3} // 节点 5 与节点 3 相连
};
cout << "DFS starting from node 0: ";
DFS(vvGraph, 0);
return 0;
}
(2) 显式栈实现
DFS 也可以通过显式栈来实现,而不是依赖递归。这种方式通常被用于避免递归过深带来的栈溢出问题:
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
void DFSIterative(const vector<vector<int>>& vvGraph, int iStartNode)
{
int iTotalNodes = vvGraph.size();
// 访问标记数组
vector<bool> vbVisited(iTotalNodes, false);
// 栈用于存储待访问的节点
stack<int> sNodes;
sNodes.push(iStartNode);
while(!sNodes.empty())
{
// 获取栈顶元素并访问
int iCurrentNode = sNodes.top();
sNodes.pop();
// 如果当前节点尚未访问,访问它
if(!vbVisited[iCurrentNode])
{
cout << iCurrentNode << " ";
vbVisited[iCurrentNode] = true;
}
// 将当前节点的邻居节点压入栈
for(int iNeighbor : vvGraph[iCurrentNode])
{
if(!vbVisited[iNeighbor])
{
sNodes.push(iNeighbor);
}
}
}
}
int main()
{
// 创建图的邻接表
vector<vector<int>> vvGraph = {
{1, 2}, // 节点 0 与节点 1、2 相连
{0, 2, 3}, // 节点 1 与节点 0、2、3 相连
{0, 1, 4}, // 节点 2 与节点 0、1、4 相连
{1, 5}, // 节点 3 与节点 1、5 相连
{2}, // 节点 4 与节点 2 相连
{3} // 节点 5 与节点 3 相连
};
cout << "Iterative DFS starting from node 0: ";
DFSIterative(vvGraph, 0);
return 0;
}
6. 代码详解
-
DFSUtil 函数(递归版):这是递归调用的核心函数。它接受当前节点、图的邻接表和访问标记数组为参数,递归地访问每个未访问的邻居。
-
DFSIterative 函数(栈版):栈版 DFS 使用栈来模拟递归的过程,每次从栈中取出一个节点,访问其邻居,并将未访问的邻居压入栈。
7. 注意事项
- 图中的环:如果图中存在环(即有重复路径),DFS 会陷入无限循环。因此,访问标记数组非常重要,用来防止重复访问节点。
- 图的连通性:如果图是非连通的,仅从一个起点进行 DFS 可能不会遍历所有节点。对于非连通图,通常需要对每个节点都进行一次 DFS。
8. 递归 vs 栈实现
- 递归实现:代码简洁,但可能由于递归深度过大而导致栈溢出。
- 显式栈实现:避免了递归深度的限制,更加健壮,但代码较为冗长。
9. DFS 的局限性
- 适用场景:DFS 更适合用于寻找解而非最优解,因为它优先遍历到最深的节点。如果需要寻找最短路径,广度优先搜索(BFS)更适合。DFS 主要用于遍历和路径存在性检测等。
10. 总结
DFS 是一种强大的遍历算法,广泛应用于图和树的遍历中。通过递归或栈的方式,它能够高效地遍历整个图,同时也为许多复杂问题提供了基础。
DFS 算法通常是图论中很多高级算法的基础。