图的深度优先遍历DFS

DFS

深度优先遍历(Depth First Search),也有称为深度优先搜索,简称为 DFS。事实上,我们在树的遍历中早已涉及 DFS------前序遍历、中序遍历和后序遍历都属于深度优先遍历的方式,因为这些遍历方式本质上都归结于栈。

DFS 过程

  1. 在图中任选一个顶点 p 作为起点,遍历它并打上标记。
  2. 选择一个 p 的没有被遍历过的邻接点,遍历并访问。如果访问到一个顶点的邻接点全部都被遍历过了,就退回到上一个访问的节点。
  3. 如果退回到起点后还有顶点没有被访问,就再任选一个没有被访问过的顶点重复操作 2。
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 以邻接矩阵存图为例
// 有 n 个点 m 条边的无权无向图
int n, m;
char v[105];         // 顶点数组
int g[105][105];     // 邻接矩阵,g[i][j] = g[j][i] = 1 表示有边
int flag[105];       // 标记某点有没有被走过

int Find(char x) {
    int i = 0;
    for (i = 1; i <= n; i++) {
        if (v[i] == x) {
            break;
        }
    }
    return i;
}

// DFS
void DFS(int i) {
    // 对 i 点进行深度优先遍历
    printf("%c ", v[i]);
    flag[i] = 1;  // 打标记
    for (int j = 1; j <= n; j++) {
        // 找 i 的未被访问过的邻接点
        if (g[i][j] == 1 && flag[j] == 0) {
            DFS(j);
        }
    }
    return;
}

int main() {
    scanf("%d %d", &n, &m);
    getchar();
    for (int i = 1; i <= n; i++) {
        scanf("%c", &v[i]);
    }
    char x, y;
    int xi, yi;
    for (int i = 1; i <= m; i++) {
        getchar();
        scanf("%c %c", &x, &y);
        xi = Find(x);
        yi = Find(y);
        g[xi][yi] = g[yi][xi] = 1;
    }

    for (int i = 1; i <= n; i++) {
        // 考虑到非连通图
        if (flag[i] == 0) {
            DFS(i);
        }
    }
    return 0;
}
/*
输入样例:
9 16
ABCDEFGHI
A B
A F
B G
G F
B C
B I
C I
C D
I D
D G
D H
D E
G H
H E
E F
F G
*/

DFS 函数解析

c 复制代码
void DFS(int i) {
    printf("%c ", v[i]);     // (1) 访问当前顶点
    flag[i] = 1;              // (2) 标记已访问
    for (int j = 1; j <= n; j++) {     // (3) 扫描所有顶点
        if (g[i][j] == 1 && flag[j] == 0) {   // (4) 找到未访问的邻接点
            DFS(j);             // (5) 递归深入
        }
    }
    return;   // (6) 回溯
}

逐行解读

作用
(1) 访问顶点 i:输出当前顶点的值,表示"已到达"。
(2) 标记已访问 :将 flag[i] 置 1,防止后续重复访问(避免死循环)。
(3) 扫描邻接点:遍历 j = 1..n,逐一检查 i 是否与 j 相连。这是邻接矩阵存图的特点------即使 i 只有少数几个邻接点,也要遍历全部 n 个顶点。
(4) 筛选条件g[i][j] == 1 表示 i 与 j 之间有边,flag[j] == 0 表示 j 尚未被访问过。只有同时满足这两个条件,j 才是"下一个要去的顶点"。
(5) 递归深入 :对 j 调用 DFS(j),从 j 继续向下探索,不再理会 i 的其他邻接点。这正是"深度优先"的含义------一条路走到黑
(6) 回溯:当 for 循环结束(即 i 的所有邻接点都已访问或不存在),函数返回上一层调用,上一层的 DFS 会继续它的 for 循环,尝试下一个邻接点。

执行流程示例

以输入样例的图为例,从顶点 A 出发(DFS(1)):

scss 复制代码
DFS(A)
  ├─ 访问 A,标记 A
  ├─ 扫描邻接点:找到 B(未访问)
  │   └─ DFS(B)
  │       ├─ 访问 B,标记 B
  │       ├─ 扫描邻接点:找到 G(未访问)
  │       │   └─ DFS(G)
  │       │       ├─ 访问 G,标记 G
  │       │       ├─ 扫描邻接点:找到 F(未访问)
  │       │       │   └─ DFS(F)
  │       │       │       ├─ 访问 F,标记 F
  │       │       │       ├─ 扫描邻接点:全部已访问 → 回溯
  │       │       │       └─ return
  │       │       ├─ 继续扫描:找到 H(未访问)
  │       │       │   └─ DFS(H)
  │       │       │       ├─ 访问 H,标记 H
  │       │       │       ├─ 扫描邻接点:找到 E(未访问)
  │       │       │       │   └─ DFS(E)
  │       │       │       │       ├─ 访问 E,标记 E
  │       │       │       │       ├─ 扫描邻接点:全部已访问 → 回溯
  │       │       │       │       └─ return
  │       │       │       ├─ 继续扫描:全部已访问 → 回溯
  │       │       │       └─ return
  │       │       ├─ 继续扫描:全部已访问 → 回溯
  │       │       └─ return
  │       ├─ 继续扫描:找到 C(未访问)
  │       │   └─ DFS(C) → ...
  │       └─ ...
  └─ ...

最终输出顺序为:A B G F H E C I D

核心机制总结

  1. 递归实现栈 :函数调用的嵌套关系天然构成一个栈,调用链有多深,栈就有多高。每次 DFS(j) 将新顶点入栈,return 时出栈(回溯)。
  2. 标记防止兜圈 :无向图中若不加标记,A → B → A 会无限循环。flag 数组保证了每个顶点有且仅有一次被访问。
  3. 回溯即"回头":当某个顶点的所有邻接点都已访问完,for 循环自然结束,函数返回------这正是 DFS 过程中"退回到上一个访问的节点"的实现方式。
  4. 非连通图处理main 函数最后的循环逐个检查 flag,确保每个连通分量都被遍历到。

时间复杂度分析

1. Find 函数

c 复制代码
int Find(char x) {
    int i = 0;
    for (i = 1; i <= n; i++) {
        if (v[i] == x) break;
    }
    return i;
}
  • 每次调用遍历顶点数组 v[1..n]最坏情况下需扫描全部 n 个顶点。
  • 时间复杂度:O(n)

2. DFS 函数

c 复制代码
void DFS(int i) {
    printf("%c ", v[i]);
    flag[i] = 1;
    for (int j = 1; j <= n; j++) {
        if (g[i][j] == 1 && flag[j] == 0) {
            DFS(j);
        }
    }
}
  • 每次调用 DFS(i) 都要执行一个 n 次的循环,检查顶点 i 与所有其他顶点之间是否有边。

  • 对于连通图,每个顶点恰好 被调用一次 DFS(因为一旦访问就会被标记,不会重复进入)。

  • 因此所有 DFS 调用的循环总次数为: \\sum_{i=1}\^{n} n = n\^2

  • 时间复杂度:O(n²)

3. main 函数整体

步骤 操作 复杂度
读入 n, m 和顶点数组 循环 n 次读入 v[i] O(n)
读入 m 条边 每读一条边调用 2 次 Find,每次 O(n) O(m · n)
遍历并调用 DFS 最坏情况每个顶点出发一次 DFS,总计 O(n²) O(n²)

总时间复杂度:O(n² + n·m)

  • 当图较稠密(m 接近 n²)时,Find 的 O(n·m) 部分可能成为瓶颈 --- 甚至可以高达 O(n³);
  • 当图较稀疏(m 远小于 n²)时,主导项是 DFS 本身的 O(n²)

4. 优化方向

上述复杂度分析基于当前邻接矩阵 + 线性查找顶点编号的实现。实际应用中可做如下优化:

优化方式 改进效果
哈希表数组下标直接映射顶点编号 Find 从 O(n) 降至 O(1),总复杂度变为 O(n² + m)
改用邻接表存图 DFS 只遍历实际存在的边,从 O(n²) 降至 O(n + m)

采用上述两种优化后,DFS 的最优时间复杂度为 O(n + m),这也是图遍历的理论下界(每个顶点和每条边至少访问一次)。

空间复杂度

  • 邻接矩阵 g[n+1][n+1]O(n²)
  • 标记数组 flag[n+1]O(n)
  • 顶点数组 v[n+1]O(n)
  • 递归调用栈深度:最坏情况下 O(n)(图退化为一条链)
  • 总空间复杂度:O(n²)

若改用邻接表,空间复杂度可降至 O(n + m)

相关推荐
青山木4 小时前
Hot 100 --- 岛屿数量
java·数据结构·算法·leetcode·深度优先·广度优先
不会就选b5 小时前
算法日常・每日刷题--<归并排序>1
数据结构·算法
危桥带雨5 小时前
排序算法(快排、归并、计数、基数排序)
数据结构·算法·排序算法
zephyr055 小时前
数据结构-并查集
数据结构
稚南城才子,乌衣巷风流6 小时前
动态树(Dynamic Tree)数据结构详解
数据结构·算法
zander2587 小时前
114. 二叉树展开为链表
数据结构·链表·深度优先
小肝一下8 小时前
3. 单链表
c语言·数据结构·c++·算法·leetcode·链表·dijkstra
tachibana28 小时前
hot100 前 K 个高频元素(347)
java·数据结构·算法·leetcode
延凡科技21 小时前
多场景落地复盘:端边云架构无人机智能巡检系统设计与实践
大数据·数据结构·人工智能·科技·架构·无人机·能源