1 拓扑序 和 DFS 回溯打印
1. 两个基础概念
- 无环有向图(DAG) :没有环路的有向图(比如 "课程先修关系":学《数据结构》前要先学《编程基础》,就可以表示为
编程基础→数据结构)。 - 拓扑排序 :对 DAG 的顶点排序,满足:若存在边
u→v,则 u 在排序结果中出现在 v 的前面(比如课程排序必须是 "编程基础" 在前,"数据结构" 在后)。
2. DFS 回溯打印的过程(结合例子)
假设我们有一个 DAG:A→B→C(A 是 B 的前驱,B 是 C 的前驱)。
DFS 遍历的步骤:
- 访问 A → 递归访问 B → 递归访问 C;
- C 没有后继了,回溯时打印 C;
- 回到 B,B 的后继(C)处理完了,回溯时打印 B;
- 回到 A,A 的后继(B)处理完了,回溯时打印 A;
- 最终输出序列:
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;
}
代码说明:
- 图的存储 :用
vector<vector<int>>实现邻接表,存储每个顶点的邻接顶点; - 栈的作用:模拟递归的 "调用栈",保存待访问的顶点;
- 逆序压栈 :因为栈是 "后进先出",为了让邻接顶点的访问顺序与递归 DFS 一致,需要逆序将邻接顶点压入栈;
- 访问标记 :
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 十字链表仅能作为有向图的一种存储结构,无向图对应的是邻接多重表
十字链表是有向图 的一种存储结构,它通过同时存储顶点的出边和入边信息,便于高效处理有向图的操作(如遍历、查找边等)。
无向图对应的链式存储结构通常是邻接多重表,而非十字链表。