跳转至

FA4-DFS-邻接矩阵

代码实现

递归

C
/**
 * 图的深度优先搜索遍历(DFS)- 邻接矩阵实现
 * 
 * 算法思想:
 * 1. 从起始顶点开始访问
 * 2. 访问一个未被访问的邻接顶点
 * 3. 以该邻接顶点为新的起始点,重复步骤2
 * 4. 当某顶点的所有邻接顶点都被访问过时,回溯到上一个顶点
 * 5. 继续寻找未被访问的邻接顶点,直到所有顶点都被访问
 * 
 * 核心特点:
 * - 使用递归实现,体现"深度优先"的思想
 * - 先纵深探索,再横向扩展
 * - 使用栈(递归调用栈)来实现回溯机制
 */

void DFS(MGraph G, int v) {
    // 访问标记数组(建议作为全局变量或静态变量)
    // 这里为了函数独立性,每次调用都重新初始化
    static int visit[maxsize] = {0};

    // 访问当前顶点(标记为已访问并输出)
    visit[v] = 1;
    printf("%d ", v);

    // 递归访问所有未被访问的邻接顶点
    for (int i = 0; i < G.numver; i++) {
        // 检查是否存在从顶点v到顶点i的边,且顶点i未被访问
        if (G.Edge[v][i] == 1 && visit[i] == 0) {
            // 递归调用DFS,深度优先探索
            DFS(G, i);
        }
    }
}

非递归

C
/**
 * 非递归版本的DFS实现(使用显式栈)
 * 
 * 算法思想:
 * 1. 将起始顶点入栈
 * 2. 当栈不为空时,弹出栈顶元素并访问
 * 3. 将该顶点的所有未访问邻接顶点入栈
 * 4. 重复直到栈为空
 */
void DFS_NonRecursive(MGraph G, int v) {
    // 访问标记数组
    int visit[maxsize] = {0};

    // 显式栈,用于模拟递归过程
    int stack[maxsize];
    int top = -1;  // 栈顶指针,-1表示空栈

    // 起始顶点入栈
    stack[++top] = v;

    // 当栈不为空时继续
    while (top != -1) {
        // 弹出栈顶元素
        v = stack[top--];

        // 如果该顶点未被访问
        if (visit[v] == 0) {
            // 访问该顶点
            visit[v] = 1;
            printf("%d ", v);

            // 将所有未访问的邻接顶点入栈(逆序入栈保证访问顺序)
            for (int i = G.numver - 1; i >= 0; i--) {
                if (G.Edge[v][i] == 1 && visit[i] == 0) {
                    stack[++top] = i;
                }
            }
        }
    }
}

复杂度分析

时间复杂度:O(V²)

  • V: 顶点数
  • 每个顶点被访问一次:O(V)
  • 对每个顶点,都要检查所有其他顶点寻找邻接点:O(V)
  • 总时间复杂度:O(V × V) = O(V²)

空间复杂度:O(V)

  • 递归版本
  • visit[]数组:O(V)
  • 递归调用栈:最坏情况O(V)(图退化为链)
  • 非递归版本
  • visit[]数组:O(V)
  • 显式栈:最坏情况O(V)
  • 总空间复杂度:O(V)

图解说明

Text Only
示例图的邻接矩阵表示:
    0 1 2 3 4
  0 0 1 1 0 0
  1 1 0 0 1 0
  2 1 0 0 1 0
  3 0 1 1 0 1
  4 0 0 0 1 0

递归DFS执行过程(从顶点0开始):

调用DFS(0):
├── 访问0,visit[0]=1
├── 查找0的邻接点:1,2
├── 调用DFS(1)  ← 优先访问第一个邻接点
│   ├── 访问1,visit[1]=1
│   ├── 查找1的邻接点:0,3
│   ├── 0已访问,调用DFS(3)
│   │   ├── 访问3,visit[3]=1
│   │   ├── 查找3的邻接点:1,2,4
│   │   ├── 1已访问,2已访问,调用DFS(4)
│   │   │   ├── 访问4,visit[4]=1
│   │   │   └── 查找4的邻接点:3(已访问)
│   │   └── DFS(4)返回
│   │   └── DFS(3)返回
│   └── DFS(1)返回
└── 回到DFS(0),继续查找0的邻接点
    └── 1已访问,2未访问,调用DFS(2)
        ├── 访问2,visit[2]=1
        └── 查找2的邻接点:0,3(都已访问)
        └── DFS(2)返回

访问顺序:0 -> 1 -> 3 -> 4 -> 2

执行轨迹图:
0 ──→ 1 ──→ 3 ──→ 4
│              ↓
└──────→ 2 ←───┘

相关知识点延伸

1. DFS与BFS的对比

特性 DFS BFS
数据结构 栈(递归) 队列
遍历方式 纵深探索 层次扩展
空间复杂度 O(V) O(V)
时间复杂度 O(V²)邻接矩阵 O(V²)邻接矩阵
最短路径 不一定最短 无权图最短路径
应用场景 拓扑排序、强连通分量 最短路径、连通性

2. DFS的经典应用

C
// 检测图中是否存在环(有向图)
int detectCycle(MGraph G) {
    int visit[maxsize] = {0};    // 访问状态
    int recStack[maxsize] = {0}; // 递归栈跟踪

    for (int i = 0; i < G.numver; i++) {
        if (isCyclicUtil(G, i, visit, recStack)) {
            return 1; // 存在环
        }
    }
    return 0; // 无环
}

3. 图的连通性分析

C
// 计算连通分量个数
int countComponents(MGraph G) {
    int visit[maxsize] = {0};
    int count = 0;

    for (int i = 0; i < G.numver; i++) {
        if (visit[i] == 0) {
            DFS(G, i);  // 访问一个连通分量
            count++;    // 连通分量数加1
        }
    }
    return count;
}

4. 拓扑排序实现

C
// 基于DFS的拓扑排序
void topologicalSort(MGraph G) {
    int visit[maxsize] = {0};
    int stack[maxsize];  // 存储拓扑序列
    int top = -1;

    // 对所有未访问顶点进行DFS
    for (int i = 0; i < G.numver; i++) {
        if (visit[i] == 0) {
            DFS_Topological(G, i, visit, stack, &top);
        }
    }

    // 输出拓扑序列
    while (top != -1) {
        printf("%d ", stack[top--]);
    }
}

5. DFS搜索树的概念

DFS遍历过程中会形成一棵搜索树: - 树边:DFS遍历中实际使用的边 - 回边:指向祖先节点的边(用于检测环) - 前向边:指向后代节点的边 - 横叉边:其他连接不同子树的边

这些概念在图论算法分析中非常重要。