第一次接触图算法时,我被DFS(深度优先搜索)和BFS(广度优先搜索)这两种截然不同的遍历方式深深吸引。想象你身处一个巨大的迷宫,DFS就像是个固执的探险家,遇到岔路就选择第一条路一直走到底,直到碰壁才回头;而BFS则像是一群训练有素的侦察兵,同时从多个方向齐头并进,确保每一步都能覆盖所有可能的路径。
在实际编码中,这两种算法都能解决图的连通性判断、路径查找等经典问题。比如社交网络中查找两个人之间的最短关系链,或者游戏中自动寻路功能的实现。但它们的实现思路和适用场景却大不相同:
下面这个简单的无向图示例可以直观展示差异:
code复制A
/ | \
B C D
\/ \/
E F
DFS可能访问顺序:A → B → E → C → D → F
BFS可能访问顺序:A → B → C → D → E → F
DFS最自然的实现方式就是递归,这完美契合了"深度优先"的思想精髓。来看这段标准实现:
c复制void DFS(Graph G, int v, int visited[]) {
visited[v] = 1;
printf("%c ", G.vexs[v]);
for (int w = first_vertex(G, v); w >= 0; w = next_vertex(G, v, w)) {
if (!visited[w]) {
DFS(G, w, visited);
}
}
}
这段代码中有几个关键点值得注意:
我在实际项目中遇到过递归深度过大的问题。当图的深度超过系统栈大小时,会导致栈溢出。这时就需要用非递归的栈实现:
c复制void DFS_NonRecursive(Graph G) {
int stack[MAX], top = -1;
int visited[MAX] = {0};
stack[++top] = 0; // 从第一个顶点开始
visited[0] = 1;
while (top >= 0) {
int v = stack[top--];
printf("%c ", G.vexs[v]);
// 逆序压栈保证访问顺序
for (int w = G.vexnum-1; w >=0; w--) {
if (G.matrix[v][w] && !visited[w]) {
visited[w] = 1;
stack[++top] = w;
}
}
}
}
DFS的性能特征很有意思:
这个特性使得DFS特别适合解决以下问题:
BFS的核心数据结构是队列,这种先进先出的特性完美实现了"广度优先"的探索策略。看这个典型实现:
c复制void BFS(Graph G) {
int queue[MAX], front = 0, rear = 0;
int visited[MAX] = {0};
queue[rear++] = 0; // 从第一个顶点开始
visited[0] = 1;
while (front != rear) {
int v = queue[front++];
printf("%c ", G.vexs[v]);
for (int w = 0; w < G.vexnum; w++) {
if (G.matrix[v][w] && !visited[w]) {
visited[w] = 1;
queue[rear++] = w;
}
}
}
}
实际编码时要注意几个细节:
BFS的性能表现:
这种特性使BFS成为解决以下问题的首选:
假设我们要判断图中两个节点是否连通,用DFS和BFS分别实现:
c复制// DFS版本
int isConnectedDFS(Graph G, int start, int end, int visited[]) {
if (start == end) return 1;
visited[start] = 1;
for (int w = first_vertex(G, start); w >= 0; w = next_vertex(G, start, w)) {
if (!visited[w] && isConnectedDFS(G, w, end, visited)) {
return 1;
}
}
return 0;
}
// BFS版本
int isConnectedBFS(Graph G, int start, int end) {
int queue[MAX], front = 0, rear = 0;
int visited[MAX] = {0};
queue[rear++] = start;
visited[start] = 1;
while (front != rear) {
int v = queue[front++];
if (v == end) return 1;
for (int w = 0; w < G.vexnum; w++) {
if (G.matrix[v][w] && !visited[w]) {
visited[w] = 1;
queue[rear++] = w;
}
}
}
return 0;
}
我们构造一个包含7个节点的网格图进行测试:
code复制A — B — C
| | |
D — E — F
|
G
测试结果对比:
| 指标 | DFS | BFS |
|---|---|---|
| 时间复杂度 | O(V+E) | O(V+E) |
| 空间复杂度 | O(V) | O(V) |
| 最短路径 | 不一定 | 保证最短 |
| 内存占用 | 栈空间 | 队列空间 |
| 适用场景 | 深层探索 | 广度扩展 |
对于存在大量重复子问题的图搜索,可以引入记忆化技术。比如在解决"图中两点间路径数"问题时:
c复制int countPathsDFS(Graph G, int start, int end, int memo[]) {
if (start == end) return 1;
if (memo[start] != -1) return memo[start];
int count = 0;
for (int w = 0; w < G.vexnum; w++) {
if (G.matrix[start][w]) {
count += countPathsDFS(G, w, end, memo);
}
}
memo[start] = count;
return count;
}
当需要查找两个点之间的路径时,双向BFS可以大幅提升效率:
c复制int bidirectionalBFS(Graph G, int start, int end) {
int queue1[MAX], front1 = 0, rear1 = 0;
int queue2[MAX], front2 = 0, rear2 = 0;
int visited1[MAX] = {0};
int visited2[MAX] = {0};
queue1[rear1++] = start;
visited1[start] = 1;
queue2[rear2++] = end;
visited2[end] = 1;
while (front1 != rear1 && front2 != rear2) {
// 正向搜索
int size1 = rear1 - front1;
while (size1--) {
int v = queue1[front1++];
if (visited2[v]) return 1;
for (int w = 0; w < G.vexnum; w++) {
if (G.matrix[v][w] && !visited1[w]) {
visited1[w] = 1;
queue1[rear1++] = w;
}
}
}
// 反向搜索
int size2 = rear2 - front2;
while (size2--) {
int v = queue2[front2++];
if (visited1[v]) return 1;
for (int w = 0; w < G.vexnum; w++) {
if (G.matrix[w][v] && !visited2[w]) {
visited2[w] = 1;
queue2[rear2++] = w;
}
}
}
}
return 0;
}
这种优化能将时间复杂度从O(b^d)降低到O(b^(d/2)),其中b是分支因子,d是起点到终点的距离。
当图的深度很大时,DFS递归版本可能会出现栈溢出。解决方法包括:
在调试图算法时,经常发现实际访问顺序与预期不符。这可能是因为:
建议的调试方法:
对于使用指针实现的邻接表,要特别注意内存释放:
c复制void freeGraph(LGraph *G) {
for (int i = 0; i < G->vexnum; i++) {
ENode *node = G->vexs[i].first_edge;
while (node) {
ENode *temp = node;
node = node->next_edge;
free(temp);
}
}
free(G);
}
在实际项目中选择DFS还是BFS,需要考虑以下因素:
问题特性:
资源限制:
数据结构影响:
并行化潜力:
在最近的一个网络拓扑分析项目中,我同时实现了两种算法。最终选择BFS作为主力方案,因为它能天然保证发现的路径是最短路径,这对网络延迟优化至关重要。而DFS则用于特殊场景下的全路径分析。