C语言图论:有向图基础

本文献给:

准备学习有向图的C语言学习者。如果你已经掌握了无向图的基础,想要理解有向图的概念和算法------本文将为你带来有向图的基础概念及简单算法。

你将学到:

  1. 有向图的基本概念和核心术语
  2. 有向图的邻接矩阵和邻接表存储方式
  3. 深度优先搜索和广度优先搜索遍历
  4. 拓扑排序、强连通分量等概念

目录

第一部分:有向图基础概念

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算法)

查找有向图中的强连通分量。算法分为三步:

  1. 对原图进行DFS,记录顶点完成时间
  2. 计算原图的转置图(所有边反向)
  3. 按完成时间逆序在转置图上进行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;
    }
}

第五部分:总结

核心要点回顾

  1. 有向图定义 : G = ( V , E ) G = (V, E) G=(V,E), E E E 是有序顶点对集合
  2. 关键概念
    • 入度 deg ⁡ − ( v ) \deg^{-}(v) deg−(v):指向顶点的边数
    • 出度 deg ⁡ + ( v ) \deg^{+}(v) deg+(v):从顶点出发的边数
    • 强连通分量:极大强连通子图
  3. 存储结构
    • 邻接矩阵:适合稠密图,快速检查边
    • 邻接表:适合稀疏图,快速获取入度/出度
  4. 遍历算法
    • DFS: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣),仅遍历出边
    • BFS: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣),计算无权图最短路径
  5. 特有算法
    • 拓扑排序:适用于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语言 #有向图 #图论 #算法 #数据结构

下一篇预告: 加权图与复杂图算法

相关推荐
枫叶丹42 小时前
【Qt开发】Qt窗口(七) -> QColorDialog 颜色对话框
c语言·开发语言·c++·qt
智者知已应修善业11 小时前
【输入两个数字,判断两数相乘是否等于各自逆序数相乘】2023-10-24
c语言·c++·经验分享·笔记·算法·1024程序员节
LaoZhangGong12311 小时前
深度学习uip中的“psock.c和psock.h”
c语言·开发语言
hefaxiang11 小时前
分支循环(下)(二)
c语言·开发语言·数据结构
小武~12 小时前
Leetcode 每日一题C 语言版 -- 45 jump game ii
c语言·算法·leetcode
LaoZhangGong12313 小时前
深度学习uip中“uip_arp.c“
c语言·stm32·以太网·arp·uip·enc28j60
laocooon52385788613 小时前
一个C项目实现框架
c语言·算法
Bona Sun14 小时前
单片机手搓掌上游戏机(二十三)—esp32运行简单街机模拟器软硬件准备
c语言·c++·单片机
zheyutao14 小时前
割点和桥
算法·图论