数据结构 12 图

1 拓扑序 和 DFS 回溯打印

1. 两个基础概念

  • 无环有向图(DAG) :没有环路的有向图(比如 "课程先修关系":学《数据结构》前要先学《编程基础》,就可以表示为编程基础→数据结构)。
  • 拓扑排序 :对 DAG 的顶点排序,满足:若存在边u→v,则 u 在排序结果中出现在 v 的前面(比如课程排序必须是 "编程基础" 在前,"数据结构" 在后)。

2. DFS 回溯打印的过程(结合例子)

假设我们有一个 DAG:A→B→C(A 是 B 的前驱,B 是 C 的前驱)。

DFS 遍历的步骤:

  1. 访问 A → 递归访问 B → 递归访问 C;
  2. C 没有后继了,回溯时打印 C
  3. 回到 B,B 的后继(C)处理完了,回溯时打印 B
  4. 回到 A,A 的后继(B)处理完了,回溯时打印 A
  5. 最终输出序列:C → B → A

3. 结论

拓扑序是A→B→C,而 DFS 回溯打印的序列是C→B→A,正好是逆拓扑有序

2 C++ 实现的图的深度优先搜索(DFS)非递归算法(以邻接表存储图):

步骤说明:

非递归 DFS 依赖来模拟递归过程,同时用数组标记顶点是否被访问。

代码实现:

cpp 复制代码
#include <iostream>
#include <vector>
#include <stack>
using namespace std;

// 图的邻接表表示
struct Graph {
    int n; // 顶点数
    vector<vector<int>> adj; // 邻接表:adj[u]存储u的邻接顶点
    vector<bool> visited;    // 标记顶点是否被访问

    // 构造函数:初始化n个顶点的图
    Graph(int vertices) : n(vertices), adj(vertices), visited(vertices, false) {}

    // 添加有向边 u -> v
    void addEdge(int u, int v) {
        adj[u].push_back(v);
    }

    // 非递归DFS算法(从起点start开始遍历)
    void dfsNonRecursive(int start) {
        stack<int> stk;
        stk.push(start);
        visited[start] = true;
        cout << "DFS遍历结果(非递归):";

        while (!stk.empty()) {
            int u = stk.top();
            stk.pop();
            cout << u << " "; // 访问当前顶点

            // 注意:栈是"后进先出",为了保证遍历顺序与递归一致,需逆序压入邻接顶点
            for (auto it = adj[u].rbegin(); it != adj[u].rend(); ++it) {
                int v = *it;
                if (!visited[v]) {
                    visited[v] = true;
                    stk.push(v);
                }
            }
        }
        cout << endl;
    }
};

// 测试示例
int main() {
    // 构造一个图(顶点编号:0~3)
    Graph g(4);
    g.addEdge(0, 1);
    g.addEdge(0, 2);
    g.addEdge(1, 3);
    g.addEdge(2, 3);

    // 从顶点0开始非递归DFS
    g.dfsNonRecursive(0);

    return 0;
}

代码说明:

  1. 图的存储 :用vector<vector<int>>实现邻接表,存储每个顶点的邻接顶点;
  2. 栈的作用:模拟递归的 "调用栈",保存待访问的顶点;
  3. 逆序压栈 :因为栈是 "后进先出",为了让邻接顶点的访问顺序与递归 DFS 一致,需要逆序将邻接顶点压入栈;
  4. 访问标记visited数组确保每个顶点只被访问一次。

测试输出:

对于示例中的图(0→1、0→2、1→3、2→3),从 0 开始的 DFS 结果为:

bash 复制代码
DFS遍历结果(非递归):0 2 3 1

3 Prim 算法和 Kruskal算法

  • Prim 算法更适合顶点数较少、边数较多的稠密图;
  • Kruskal 算法更适合边数较少、顶点数较多的稀疏图。

一、Prim 算法

  • 核心思路:从一个起点顶点出发,每次选择 "当前已选顶点集合" 与 "未选顶点集合" 之间权值最小的边,将对应的未选顶点加入已选集合,直到所有顶点都被加入。
  • 适用场景 :更适合稠密图(顶点少、边多),因为其时间复杂度由顶点数主导。
  • 实现依赖:通常基于邻接矩阵存储图,配合数组记录顶点的最小边权。

二、Kruskal 算法

  • 核心思路:先将所有边按权值从小到大排序,然后依次选边,若选的边不会使已选边构成环(用并查集判断),则保留该边,直到选够\(n-1\)条边(n为顶点数)。
  • 适用场景 :更适合稀疏图(顶点多、边少),因为其时间复杂度由边数主导。
  • 实现依赖:需要对边排序,同时借助并查集高效判断环。

4 强连通分量

强连通分量是针对有向图 的概念:在一个有向图中,若某个子图里的任意两个节点之间都能互相到达(比如节点 A 能到 B,B 也能到 A),这个子图就是一个强连通分量。

举个例子:如果有向图里有节点 A→B、B→C、C→A,那么 {A,B,C} 就是一个强连通分量(互相可达);但如果只有 A→B、B→C,就不是(C 到不了 A)。

对于无向图,没有 "强连通分量" 的说法,无向图里 "任意两点可达" 的子图叫连通分量

5 邻接矩阵可以存储带权的有向图和无向图

  • 对于带权图,邻接矩阵中元素的值表示边的权值;
  • 若两点间无边,则用特定值(如 0 或∞)表示。

邻接表是存储带权图的方式之一,但并非唯一方式,邻接矩阵同样支持带权图的存储。

6 十字链表仅能作为有向图的一种存储结构,无向图对应的是邻接多重表

十字链表是有向图 的一种存储结构,它通过同时存储顶点的出边和入边信息,便于高效处理有向图的操作(如遍历、查找边等)。

无向图对应的链式存储结构通常是邻接多重表,而非十字链表。

相关推荐
oraen17 分钟前
【AI学习-2.1】部署自己的本地大模型 -本地推理
学习
不如自挂东南吱31 分钟前
空间相关性 和 怎么捕捉空间相关性
人工智能·深度学习·算法·机器学习·时序数据库
لا معنى له34 分钟前
学习笔记:Restormer: Efficient Transformer for High-Resolution Image Restoration
图像处理·笔记·学习·计算机视觉·transformer
claider40 分钟前
Vim User Manual 阅读笔记 Usr_05.txt Set your settings 设置你的设置
笔记·编辑器·vim
荒诞硬汉1 小时前
对象数组.
java·数据结构
科技林总1 小时前
【系统分析师】3.4 指令系统
学习
洛生&1 小时前
Elevator Rides
算法
li星野1 小时前
OpenCV4.X学习-视频相关
学习·音视频
2501_933513041 小时前
关于一种计数的讨论、ARC212C Solution
算法
Wu_Dylan1 小时前
智能体系列(二):规划(Planning):从 CoT、ToT 到动态采样与搜索
人工智能·算法