本文献给:
准备学习有向图的C语言学习者。如果你已经掌握了无向图的基础,想要理解有向图的概念和算法------本文将为你带来有向图的基础概念及简单算法。
你将学到:
- 有向图的基本概念和核心术语
- 有向图的邻接矩阵和邻接表存储方式
- 深度优先搜索和广度优先搜索遍历
- 拓扑排序、强连通分量等概念
目录
- 第一部分:有向图基础概念
-
- [1. 什么是有向图?](#1. 什么是有向图?)
- [2. 基本术语](#2. 基本术语)
- 第二部分:存储方式
-
- [1. 邻接矩阵](#1. 邻接矩阵)
- [2. 邻接表](#2. 邻接表)
- [3. 存储方式对比](#3. 存储方式对比)
- 第三部分:图的遍历
-
- [1. 深度优先搜索(DFS)](#1. 深度优先搜索(DFS))
- [2. 广度优先搜索(BFS)](#2. 广度优先搜索(BFS))
- 第四部分:有向图特有算法
-
- [1. 拓扑排序](#1. 拓扑排序)
- [2. 强连通分量(Kosaraju算法)](#2. 强连通分量(Kosaraju算法))
- [3. 有向图最短路径(Dijkstra算法)](#3. 有向图最短路径(Dijkstra算法))
- 第五部分:总结
第一部分:有向图基础概念
1. 什么是有向图?
有向图 G = ( V , E ) G = (V, E) G=(V,E) 由顶点集合 V V V 和有向边集合 E E E 组成,其中每条边 e ∈ E e \in E e∈E 是有序的顶点对 ( u , v ) (u, v) (u,v),表示从 u u u 指向 v v v 的单向连接。
数学定义:
G = ( V , E ) 其中 E ⊆ { ( u , v ) ∣ u , v ∈ V , u ≠ v } G = (V, E) \quad \text{其中} \quad E \subseteq \{ (u, v) \mid u, v \in V, u \neq v \} G=(V,E)其中E⊆{(u,v)∣u,v∈V,u=v}
示例:
顶点集合:V = {A, B, C, D}
边集合:E = {(A→B), (A→C), (B→C), (C→D)}
2. 基本术语
- 顶点(Vertex) :图的基本元素, v ∈ V v \in V v∈V
- 有向边(Directed Edge) :从 u u u 指向 v v v 的箭头, e = ( u , v ) ∈ E e = (u, v) \in E e=(u,v)∈E
- 入度(In-degree) : deg − ( v ) = ∣ { u ∈ V ∣ ( u , v ) ∈ E } ∣ \deg^{-}(v) = |\{ u \in V \mid (u, v) \in E \}| deg−(v)=∣{u∈V∣(u,v)∈E}∣
- 出度(Out-degree) : deg + ( v ) = ∣ { u ∈ V ∣ ( v , u ) ∈ E } ∣ \deg^{+}(v) = |\{ u \in V \mid (v, u) \in E \}| deg+(v)=∣{u∈V∣(v,u)∈E}∣
- 度数性质 : ∑ v ∈ V deg − ( v ) = ∑ v ∈ V deg + ( v ) = ∣ E ∣ \sum_{v \in V} \deg^{-}(v) = \sum_{v \in V} \deg^{+}(v) = |E| ∑v∈Vdeg−(v)=∑v∈Vdeg+(v)=∣E∣
- 路径(Path) :顶点序列 v 0 , v 1 , . . . , v k v_0, v_1, ..., v_k v0,v1,...,vk,其中 ( v i , v i + 1 ) ∈ E (v_i, v_{i+1}) \in E (vi,vi+1)∈E
- 强连通图 : ∀ u , v ∈ V \forall u, v \in V ∀u,v∈V,存在从 u u u 到 v v v 和从 v v v 到 u u u 的路径
第二部分:存储方式
1. 邻接矩阵
使用 n × n n \times n n×n 矩阵 A A A 存储,其中 n = ∣ V ∣ n = |V| n=∣V∣:
A [ i ] [ j ] = { 1 if ( i , j ) ∈ E 0 otherwise A[i][j] = \begin{cases} 1 & \text{if } (i,j) \in E \\ 0 & \text{otherwise} \end{cases} A[i][j]={10if (i,j)∈Eotherwise
c
#define MAX_V 100
typedef struct {
int matrix[MAX_V][MAX_V]; // 邻接矩阵
int vertex_count; // |V|
int edge_count; // |E|
} DigraphMatrix;
// 初始化:时间复杂度 O(n²),空间复杂度 O(n²)
void init_digraph(DigraphMatrix* g, int n) {
g->vertex_count = n;
g->edge_count = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
g->matrix[i][j] = 0;
}
}
}
// 添加有向边:时间复杂度 O(1)
void add_directed_edge(DigraphMatrix* g, int from, int to) {
if (from >= g->vertex_count || to >= g->vertex_count) return;
g->matrix[from][to] = 1; // 有向图,非对称
g->edge_count++;
}
// 获取顶点入度:时间复杂度 O(n)
int get_in_degree(DigraphMatrix* g, int v) {
int degree = 0;
for (int i = 0; i < g->vertex_count; i++) {
if (g->matrix[i][v]) degree++;
}
return degree;
}
// 获取顶点出度:时间复杂度 O(n)
int get_out_degree(DigraphMatrix* g, int v) {
int degree = 0;
for (int i = 0; i < g->vertex_count; i++) {
if (g->matrix[v][i]) degree++;
}
return degree;
}
2. 邻接表
对于每个顶点 v v v,维护其出边 邻居列表 A d j [ v ] = { u ∈ V ∣ ( v , u ) ∈ E } Adj[v] = \{ u \in V \mid (v, u) \in E \} Adj[v]={u∈V∣(v,u)∈E}
c
typedef struct Node {
int vertex;
struct Node* next;
} Node;
typedef struct {
Node* lists[MAX_V]; // 出边邻接表
int in_degree[MAX_V]; // 入度数组
int out_degree[MAX_V]; // 出度数组
int vertex_count; // |V|
int edge_count; // |E|
} DigraphList;
// 添加有向边:时间复杂度 O(1)
void add_directed_edge_list(DigraphList* g, int from, int to) {
// 添加到from的出边邻接表
Node* node = create_node(to);
node->next = g->lists[from];
g->lists[from] = node;
// 更新入度和出度
g->out_degree[from]++;
g->in_degree[to]++;
g->edge_count++;
}
// 获取顶点入度:时间复杂度 O(1)
int get_in_degree_list(DigraphList* g, int v) {
return g->in_degree[v];
}
// 获取顶点出度:时间复杂度 O(1)
int get_out_degree_list(DigraphList* g, int v) {
return g->out_degree[v];
}
3. 存储方式对比
| 特性 | 邻接矩阵 | 邻接表 | 时间复杂度 |
|---|---|---|---|
| 空间 | $O( | V | ^2)$ |
| 检查边 ( u → v ) (u→v) (u→v) | O ( 1 ) O(1) O(1) | O ( deg + ( u ) ) O(\deg^{+}(u)) O(deg+(u)) | 邻接矩阵更快 |
| 遍历 v v v 的出边邻居 | $O( | V | )$ |
| 获取入度 | $O( | V | )$ |
| 获取出度 | $O( | V | )$ |
| 添加边 | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) | 相当 |
选择建议:
- 频繁查询入度/出度:邻接表
- 频繁检查边是否存在:邻接矩阵
- 稠密图:邻接矩阵
- 稀疏图:邻接表
第三部分:图的遍历
1. 深度优先搜索(DFS)
沿着有向边深入到底再回溯,仅遍历出边。
算法复杂度:
- 邻接矩阵: O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
- 邻接表: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣)
c
// DFS递归实现(遍历出边)
void dfs_digraph(DigraphMatrix* g, int v, int visited[]) {
visited[v] = 1;
for (int i = 0; i < g->vertex_count; i++) {
if (g->matrix[v][i] && !visited[i]) {
dfs_digraph(g, i, visited);
}
}
}
// DFS迭代实现(栈)
void dfs_digraph_iterative(DigraphMatrix* g, int start) {
int visited[MAX_V] = {0};
int stack[MAX_V];
int top = -1;
stack[++top] = start;
while (top >= 0) {
int v = stack[top--];
if (!visited[v]) {
visited[v] = 1;
// 逆序压栈保持与递归相同顺序
for (int i = g->vertex_count - 1; i >= 0; i--) {
if (g->matrix[v][i] && !visited[i]) {
stack[++top] = i;
}
}
}
}
}
2. 广度优先搜索(BFS)
逐层扩展,仅遍历出边。
算法复杂度:
- 邻接矩阵: O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
- 邻接表: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣)
c
// BFS队列实现(遍历出边)
void bfs_digraph(DigraphMatrix* g, int start) {
int visited[MAX_V] = {0};
int queue[MAX_V];
int front = 0, rear = 0;
visited[start] = 1;
queue[rear++] = start;
while (front < rear) {
int v = queue[front++];
for (int i = 0; i < g->vertex_count; i++) {
if (g->matrix[v][i] && !visited[i]) {
visited[i] = 1;
queue[rear++] = i;
}
}
}
}
// BFS计算单源最短路径(无权有向图)
void bfs_shortest_path_digraph(DigraphMatrix* g, int start, int distance[]) {
for (int i = 0; i < g->vertex_count; i++) {
distance[i] = -1; // -1表示不可达
}
int visited[MAX_V] = {0};
int queue[MAX_V];
int front = 0, rear = 0;
visited[start] = 1;
distance[start] = 0;
queue[rear++] = start;
while (front < rear) {
int v = queue[front++];
for (int i = 0; i < g->vertex_count; i++) {
if (g->matrix[v][i] && !visited[i]) {
visited[i] = 1;
distance[i] = distance[v] + 1;
queue[rear++] = i;
}
}
}
}
第四部分:有向图特有算法
1. 拓扑排序
对有向无环图(DAG)的顶点进行线性排序,使得对于每条有向边 ( u , v ) (u, v) (u,v), u u u 在排序中都在 v v v 之前。
算法复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣)
c
// DFS辅助函数,用于拓扑排序
void topological_sort_dfs(DigraphMatrix* g, int v, int visited[], int stack[], int* top) {
visited[v] = 1;
for (int i = 0; i < g->vertex_count; i++) {
if (g->matrix[v][i] && !visited[i]) {
topological_sort_dfs(g, i, visited, stack, top);
}
}
// 递归完成后将顶点压入栈
stack[++(*top)] = v;
}
// Kahn算法(基于入度的拓扑排序)
void topological_sort_kahn(DigraphList* g, int result[]) {
int indegree[MAX_V];
int queue[MAX_V];
int front = 0, rear = 0;
int index = 0;
// 初始化入度数组
for (int i = 0; i < g->vertex_count; i++) {
indegree[i] = g->in_degree[i];
if (indegree[i] == 0) {
queue[rear++] = i;
}
}
// Kahn算法主循环
while (front < rear) {
int v = queue[front++];
result[index++] = v;
// 遍历v的所有出边邻居
Node* curr = g->lists[v];
while (curr != NULL) {
int neighbor = curr->vertex;
indegree[neighbor]--;
if (indegree[neighbor] == 0) {
queue[rear++] = neighbor;
}
curr = curr->next;
}
}
// 检查是否有环
if (index != g->vertex_count) {
// 存在环,无法进行拓扑排序
result[0] = -1; // 使用-1表示错误
}
}
2. 强连通分量(Kosaraju算法)
查找有向图中的强连通分量。算法分为三步:
- 对原图进行DFS,记录顶点完成时间
- 计算原图的转置图(所有边反向)
- 按完成时间逆序在转置图上进行DFS
算法复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣)
c
// Kosaraju算法第一步:记录完成时间
void fill_order(DigraphMatrix* g, int v, int visited[], int stack[], int* top) {
visited[v] = 1;
for (int i = 0; i < g->vertex_count; i++) {
if (g->matrix[v][i] && !visited[i]) {
fill_order(g, i, visited, stack, top);
}
}
// 顶点v的所有邻居都访问完成后,将v压入栈
stack[++(*top)] = v;
}
// 获得转置图(所有边反向)
void get_transpose(DigraphMatrix* src, DigraphMatrix* dst) {
init_digraph(dst, src->vertex_count);
for (int i = 0; i < src->vertex_count; i++) {
for (int j = 0; j < src->vertex_count; j++) {
if (src->matrix[i][j]) {
add_directed_edge(dst, j, i); // 边反向
}
}
}
}
// Kosaraju算法:查找强连通分量
void kosaraju_scc(DigraphMatrix* g, int components[]) {
int stack[MAX_V];
int top = -1;
int visited[MAX_V] = {0};
// 第一步:记录完成时间
for (int i = 0; i < g->vertex_count; i++) {
if (!visited[i]) {
fill_order(g, i, visited, stack, &top);
}
}
// 第二步:获取转置图
DigraphMatrix transposed;
get_transpose(g, &transposed);
// 第三步:重置visited数组
for (int i = 0; i < g->vertex_count; i++) visited[i] = 0;
// 第四步:按完成时间逆序在转置图上DFS
int component_id = 0;
while (top >= 0) {
int v = stack[top--];
if (!visited[v]) {
// 开始一个新的强连通分量
component_id++;
// 使用栈进行DFS
int dfs_stack[MAX_V];
int dfs_top = -1;
dfs_stack[++dfs_top] = v;
while (dfs_top >= 0) {
int u = dfs_stack[dfs_top--];
if (!visited[u]) {
visited[u] = 1;
components[u] = component_id;
for (int i = transposed.vertex_count - 1; i >= 0; i--) {
if (transposed.matrix[u][i] && !visited[i]) {
dfs_stack[++dfs_top] = i;
}
}
}
}
}
}
}
3. 有向图最短路径(Dijkstra算法)
计算从源点到所有其他顶点的最短路径,要求边权非负。
算法复杂度:
- 朴素实现: O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
- 优先队列优化: O ( ∣ E ∣ + ∣ V ∣ log ∣ V ∣ ) O(|E| + |V|\log|V|) O(∣E∣+∣V∣log∣V∣)
c
#define INF INT_MAX
// Dijkstra算法朴素实现
void dijkstra_shortest_path(DigraphMatrix* g, int start, int distance[], int parent[]) {
int visited[MAX_V] = {0};
// 初始化
for (int i = 0; i < g->vertex_count; i++) {
distance[i] = INF;
parent[i] = -1;
}
distance[start] = 0;
// 找到从源点到所有顶点的最短路径
for (int count = 0; count < g->vertex_count - 1; count++) {
// 选择未访问顶点中距离最小的
int min_distance = INF;
int u = -1;
for (int v = 0; v < g->vertex_count; v++) {
if (!visited[v] && distance[v] < min_distance) {
min_distance = distance[v];
u = v;
}
}
if (u == -1) break; // 剩余顶点不可达
visited[u] = 1;
// 更新u的所有邻居的距离
for (int v = 0; v < g->vertex_count; v++) {
// 假设matrix[u][v]存储边的权重
if (g->matrix[u][v] && !visited[v]) {
int new_distance = distance[u] + g->matrix[u][v];
if (new_distance < distance[v]) {
distance[v] = new_distance;
parent[v] = u;
}
}
}
}
}
// 重建最短路径
void reconstruct_path(int parent[], int target, int path[], int* length) {
*length = 0;
int current = target;
// 从目标顶点回溯到源点
while (current != -1) {
path[(*length)++] = current;
current = parent[current];
}
// 反转路径(从源点到目标顶点)
for (int i = 0; i < *length / 2; i++) {
int temp = path[i];
path[i] = path[*length - i - 1];
path[*length - i - 1] = temp;
}
}
第五部分:总结
核心要点回顾
- 有向图定义 : G = ( V , E ) G = (V, E) G=(V,E), E E E 是有序顶点对集合
- 关键概念 :
- 入度 deg − ( v ) \deg^{-}(v) deg−(v):指向顶点的边数
- 出度 deg + ( v ) \deg^{+}(v) deg+(v):从顶点出发的边数
- 强连通分量:极大强连通子图
- 存储结构 :
- 邻接矩阵:适合稠密图,快速检查边
- 邻接表:适合稀疏图,快速获取入度/出度
- 遍历算法 :
- DFS: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣),仅遍历出边
- BFS: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣),计算无权图最短路径
- 特有算法 :
- 拓扑排序:适用于DAG, O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣)
- 强连通分量:Kosaraju算法, O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣)
- 最短路径:Dijkstra算法(边权非负), O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
标签: #C语言 #有向图 #图论 #算法 #数据结构
下一篇预告: 加权图与复杂图算法