数据结构 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 十字链表仅能作为有向图的一种存储结构,无向图对应的是邻接多重表

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

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

相关推荐
程序员-周李斌42 分钟前
LinkedList 源码深度分析(基于 JDK 8)
java·开发语言·数据结构·list
咫尺的梦想00743 分钟前
链表——删除链表的倒数第 N 个结点
数据结构·链表
梁bk1 小时前
Redis底层数据结构 -- ziplist, quicklist, skiplist
数据结构·数据库·redis
BOF_dcb1 小时前
乘法原理+除法原理
笔记
STY_fish_20121 小时前
P11855 [CSP-J2022 山东] 部署
算法·图论·差分
myw0712051 小时前
湘大头歌程-Ride to Office练习笔记
c语言·数据结构·笔记·算法
H_BB1 小时前
算法详解:滑动窗口机制
数据结构·c++·算法·滑动窗口
淀粉肠kk1 小时前
【C++】封装红黑树实现Mymap和Myset
数据结构·c++
Zero-Talent2 小时前
“栈” 算法
算法