跳转至

FA5-DFS-邻接表

代码实现

递归版本

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

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

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

    // 获取当前顶点的第一条邻接边
    Node p = G.adjlist[v].firstarc;

    // 遍历当前顶点的所有邻接边
    for (; p != NULL; p = p->nextarc) {
        // 检查邻接顶点是否未被访问
        if (visit[p->adjvex] == 0) {
            // 递归调用DFS,深度优先探索邻接顶点
            DFS(G, p->adjvex);
        }
    }
}

非递归版本

C
/**
 * 非递归版本的DFS实现(使用显式栈)- 邻接表版本
 * 
 * 算法思想:
 * 1. 将起始顶点入栈
 * 2. 当栈不为空时,弹出栈顶元素并访问
 * 3. 将该顶点的所有未访问邻接顶点入栈
 * 4. 重复直到栈为空
 */
void DFS_NonRecursive(Graph 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);

            // 将所有未访问的邻接顶点入栈
            // 注意:邻接表是链式结构,需要逆序遍历保证访问顺序
            Node p = G.adjlist[v].firstarc;
            // 先将所有邻接点存入临时数组
            int adj_vertices[maxsize];
            int count = 0;

            for (; p != NULL; p = p->nextarc) {
                if (visit[p->adjvex] == 0) {
                    adj_vertices[count++] = p->adjvex;
                }
            }

            // 逆序入栈(保证访问顺序的一致性)
            for (int i = count - 1; i >= 0; i--) {
                stack[++top] = adj_vertices[i];
            }
        }
    }
}
C
/**
 * 更简洁的非递归DFS实现
 * 直接按邻接表顺序入栈(访问顺序可能与递归版本不同)
 */
void DFS_NonRecursive_Simple(Graph G, int v) {
    int visit[maxsize] = {0};
    int stack[maxsize];
    int top = -1;

    stack[++top] = v;

    while (top != -1) {
        v = stack[top--];

        if (visit[v] == 0) {
            visit[v] = 1;
            printf("%d ", v);

            // 直接按邻接表顺序将邻接点入栈
            Node p = G.adjlist[v].firstarc;
            for (; p != NULL; p = p->nextarc) {
                if (visit[p->adjvex] == 0) {
                    stack[++top] = p->adjvex;
                }
            }
        }
    }
}

复杂度分析

时间复杂度:O(V + E)

  • V: 顶点数,E: 边数
  • 每个顶点被访问一次:O(V)
  • 每条边被检查一次:O(E)
  • 总时间复杂度:O(V + E)

空间复杂度:O(V)

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

图解说明

Text Only
示例图结构(邻接表表示):
0 -> 1 -> 2
1 -> 0 -> 3
2 -> 0 -> 3
3 -> 1 -> 2 -> 4
4 -> 3

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

调用DFS(0):
├── 访问0,visit[0]=1
├── 遍历0的邻接表:1 -> 2
├── 调用DFS(1)  ← 按邻接表顺序,先访问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
└──────────┘

邻接表遍历过程:
顶点0: [1] -> [2]  (按链表顺序访问)
顶点1: [0] -> [3]  (0已访问,访问3)
顶点3: [1] -> [2] -> [4] (1,2已访问,访问4)
顶点4: [3] (3已访问,回溯)
顶点2: [0] -> [3] (0,3已访问,回溯)

相关知识点延伸

1. 邻接表与邻接矩阵DFS的比较

特性 邻接表DFS 邻接矩阵DFS
时间复杂度 O(V + E) O(V²)
空间复杂度 O(V + E) O(V²)
适用图类型 稀疏图 稠密图
邻接点查找 O(degree) O(V)
实现复杂度 较复杂 较简单

2. DFS的四种边类型(有向图)

C
1
2
3
4
5
6
7
// 在DFS过程中可以识别四种边类型
enum EdgeType {
    TREE_EDGE,     // 树边:DFS树中的边
    BACK_EDGE,     // 回边:指向祖先的边(检测环)
    FORWARD_EDGE,  // 前向边:指向后代的边
    CROSS_EDGE     // 横叉边:其他边
};

3. DFS在有向图中的应用

C
// 检测有向图中的环
int isCyclic(Graph G) {
    int color[maxsize] = {0}; // 0:未访问, 1:正在访问, 2:已完成访问

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

int isCyclicUtil(Graph G, int v, int color[]) {
    color[v] = 1; // 标记为正在访问

    Node p = G.adjlist[v].firstarc;
    for (; p != NULL; p = p->nextarc) {
        if (color[p->adjvex] == 1) {
            return 1; // 发现回边,存在环
        }
        if (color[p->adjvex] == 0 && isCyclicUtil(G, p->adjvex, color)) {
            return 1;
        }
    }

    color[v] = 2; // 标记为已完成访问
    return 0;
}

4. 强连通分量算法(Kosaraju算法)

C
// 使用两次DFS计算强连通分量
void findSCC(Graph G) {
    int visited[maxsize] = {0};
    int stack[maxsize];
    int top = -1;

    // 第一次DFS:按完成时间排序
    for (int i = 0; i < G.numver; i++) {
        if (!visited[i]) {
            DFS_First(G, i, visited, stack, &top);
        }
    }

    // 构造转置图
    Graph GT = transposeGraph(G);

    // 重置访问标记
    memset(visited, 0, sizeof(visited));

    // 第二次DFS:按完成时间逆序访问转置图
    while (top != -1) {
        int v = stack[top--];
        if (!visited[v]) {
            printf("SCC: ");
            DFS(GT, v);  // 每次DFS访问一个强连通分量
            printf("\n");
        }
    }
}

5. DFS生成树和应用

DFS遍历会生成DFS生成树,这个树在以下方面有重要应用: - 关节点检测:删除后图不再连通的顶点 - 桥边检测:删除后图不再连通的边 - 双连通分量:不含关节点的极大子图 - 欧拉回路:经过每条边恰好一次的回路

这些概念构成了图论算法的重要基础。