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

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

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

相关推荐
Hunter11610 小时前
Delphi通过ITHTTP传输有汉字乱码问题
笔记
珂朵莉MM10 小时前
第七届全球校园人工智能算法精英大赛-算法巅峰赛产业命题赛第一赛季优化题--无人机配送
人工智能·算法·无人机
有为少年10 小时前
带噪学习 | Ambient Diffusion (NeurIPS 2023)下篇
人工智能·深度学习·神经网络·学习·机器学习·计算机视觉
xiaoxue..11 小时前
列表转树结构:从扁平列表到层级森林
前端·javascript·算法·面试
专注API从业者11 小时前
构建企业级 1688 数据管道:商品详情 API 的分布式采集与容错设计
大数据·开发语言·数据结构·数据库·分布式
代码游侠11 小时前
复习——线程(pthread)
linux·运维·开发语言·网络·学习·算法
papaofdoudou11 小时前
基于QEMU 模拟intel-iommu的sva/svm demo环境搭建和验证
算法·机器学习·支持向量机
再__努力1点11 小时前
【78】HOG+SVM行人检测实践指南:从算法原理到python实现
开发语言·人工智能·python·算法·机器学习·支持向量机·计算机视觉
做cv的小昊11 小时前
【TJU】信息检索与分析课程笔记和练习(3)学术评价
大数据·人工智能·经验分享·笔记·学习·全文检索
scx2013100411 小时前
20251214 字典树总结
算法·字典树