7-6 拓扑排序(使用入度数组)
拓扑排序(使用入度数组 / Kahn's Algorithm)
拓扑排序(Topological Sort)是将有向无环图(Directed Acyclic Graph, DAG)中所有顶点排成一个线性序列,使得对于图中的每一条有向边 u -> v,顶点 u 在序列中都排在顶点 v 的前面。也就是说,如果存在一条从 u 到 v 的路径,那么 u 一定出现在 v 之前。
Kahn 算法(Kahn's Algorithm)是实现拓扑排序的经典方法,其核心思想基于入度(Indegree)——即指向某顶点的边的数量。算法步骤为:首先计算所有顶点的入度,将入度为 0 的顶点入队;然后不断取出队首顶点,将其所有邻居的入度减 1,若某邻居入度变为 0 则入队。最终,如果所有顶点都被处理,则得到一个合法的拓扑序列;否则说明图中存在环(Cycle)。
本文使用如下有向无环图(6 个顶点):
边(Edges):
(5, 2), (5, 0), (4, 0), (4, 1), (2, 3), (3, 1)
图的结构:
5 ----> 2 ----> 3
| |
v v
0 <---- 4 ----> 1
入度(Indegree):
0: 2 (from 5 and 4)
1: 2 (from 4 and 3)
2: 1 (from 5)
3: 1 (from 2)
4: 0
5: 0
一个合法的拓扑排序: 4 5 0 2 3 1
图的表示(有向图)
对于有向图(Directed Graph),邻接表只需存储一个方向:每个顶点指向的所有邻居。不需要像无向图那样双向添加。C++ 使用 vector<vector<int>>,C 语言使用二维数组加计数数组,Python 使用字典嵌套列表,Go 使用 [][]int 切片。
#include <iostream>
#include <vector>
using namespace std;
int main() {
// Build directed graph using adjacency list
// adj[u] = vertices that u points to
int n = 6;
vector<vector<int>> adj(n);
// Directed edges: u -> v
adj[5].push_back(2);
adj[5].push_back(0);
adj[4].push_back(0);
adj[4].push_back(1);
adj[2].push_back(3);
adj[3].push_back(1);
// Print adjacency list
for (int i = 0; i < n; i++) {
cout << i << ": ";
for (int v : adj[i]) {
cout << v << " ";
}
cout << endl;
}
return 0;
}
#include <stdio.h>
#define MAX_NODES 6
#define MAX_NEIGHBORS 4
int main() {
// Use 2D array to represent directed adjacency list
int adj[MAX_NODES][MAX_NEIGHBORS] = {0};
int adjCount[MAX_NODES] = {0};
// Helper macro to add directed edge u -> v
#define ADD_EDGE(u, v) do { \
adj[u][adjCount[u]++] = v; \
} while(0)
// Directed edges
ADD_EDGE(5, 2);
ADD_EDGE(5, 0);
ADD_EDGE(4, 0);
ADD_EDGE(4, 1);
ADD_EDGE(2, 3);
ADD_EDGE(3, 1);
// Print adjacency list
for (int i = 0; i < MAX_NODES; i++) {
printf("%d: ", i);
for (int j = 0; j < adjCount[i]; j++) {
printf("%d ", adj[i][j]);
}
printf("\n");
}
return 0;
}
def main():
# Build directed graph using adjacency list
adj = {i: [] for i in range(6)}
# Directed edges: u -> v
adj[5] = [2, 0]
adj[4] = [0, 1]
adj[2] = [3]
adj[3] = [1]
# Print adjacency list
for i in range(6):
print(f"{i}: {adj[i]}")
if __name__ == "__main__":
main()
package main
import "fmt"
func main() {
// Build directed graph using adjacency list
adj := make([][]int, 6)
// Directed edges: u -> v
adj[5] = append(adj[5], 2, 0)
adj[4] = append(adj[4], 0, 1)
adj[2] = append(adj[2], 3)
adj[3] = append(adj[3], 1)
// Print adjacency list
for i := 0; i < 6; i++ {
fmt.Printf("%d: %v\n", i, adj[i])
}
}
上述代码构建了示例有向图的邻接表表示。与无向图不同,有向图的边只存储一个方向。C++ 使用 vector<vector<int>>;C 语言用固定大小的二维数组配合计数数组;Python 使用列表的列表;Go 使用 [][]int 切片。
运行该程序将输出:
0:
1:
2: 3
3: 1
4: 0 1
5: 2 0
计算入度
入度(Indegree)是指向某个顶点的有向边的数量。对于顶点 v,其入度等于所有满足 u -> v 的顶点 u 的个数。计算入度的方法是遍历邻接表中所有顶点的邻居,每遇到一个邻居 v,就将 v 的入度加 1。
在示例图中,入度计算过程为:
遍历邻接表:
5 -> 2: indegree[2]++
5 -> 0: indegree[0]++
4 -> 0: indegree[0]++
4 -> 1: indegree[1]++
2 -> 3: indegree[3]++
3 -> 1: indegree[1]++
结果:
indegree[0] = 2 (来自5和4)
indegree[1] = 2 (来自4和3)
indegree[2] = 1 (来自5)
indegree[3] = 1 (来自2)
indegree[4] = 0
indegree[5] = 0
C++ 实现
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n = 6;
vector<vector<int>> adj(n);
adj[5].push_back(2);
adj[5].push_back(0);
adj[4].push_back(0);
adj[4].push_back(1);
adj[2].push_back(3);
adj[3].push_back(1);
// Compute indegree for each vertex
vector<int> indegree(n, 0);
for (int u = 0; u < n; u++) {
for (int v : adj[u]) {
indegree[v]++;
}
}
// Print indegree of each vertex
for (int i = 0; i < n; i++) {
cout << "indegree[" << i << "] = " << indegree[i] << endl;
}
return 0;
}
C 实现
#include <stdio.h>
#define MAX_NODES 6
#define MAX_NEIGHBORS 4
int main() {
int adj[MAX_NODES][MAX_NEIGHBORS] = {0};
int adjCount[MAX_NODES] = {0};
#define ADD_EDGE(u, v) do { \
adj[u][adjCount[u]++] = v; \
} while(0)
ADD_EDGE(5, 2);
ADD_EDGE(5, 0);
ADD_EDGE(4, 0);
ADD_EDGE(4, 1);
ADD_EDGE(2, 3);
ADD_EDGE(3, 1);
// Compute indegree for each vertex
int indegree[MAX_NODES] = {0};
for (int u = 0; u < MAX_NODES; u++) {
for (int j = 0; j < adjCount[u]; j++) {
indegree[adj[u][j]]++;
}
}
// Print indegree of each vertex
for (int i = 0; i < MAX_NODES; i++) {
printf("indegree[%d] = %d\n", i, indegree[i]);
}
return 0;
}
Python 实现
def main():
adj = {i: [] for i in range(6)}
adj[5] = [2, 0]
adj[4] = [0, 1]
adj[2] = [3]
adj[3] = [1]
# Compute indegree for each vertex
indegree = {i: 0 for i in range(6)}
for u in range(6):
for v in adj[u]:
indegree[v] += 1
# Print indegree of each vertex
for i in range(6):
print(f"indegree[{i}] = {indegree[i]}")
if __name__ == "__main__":
main()
Go 实现
package main
import "fmt"
func main() {
adj := make([][]int, 6)
adj[5] = append(adj[5], 2, 0)
adj[4] = append(adj[4], 0, 1)
adj[2] = append(adj[2], 3)
adj[3] = append(adj[3], 1)
// Compute indegree for each vertex
indegree := make([]int, 6)
for u := 0; u < 6; u++ {
for _, v := range adj[u] {
indegree[v]++
}
}
// Print indegree of each vertex
for i := 0; i < 6; i++ {
fmt.Printf("indegree[%d] = %d\n", i, indegree[i])
}
}
上述代码遍历邻接表中所有顶点的出边,统计每个顶点的入度。入度是 Kahn 算法的基础——入度为 0 的顶点没有前置依赖,可以最先被处理。
运行该程序将输出:
indegree[0] = 2
indegree[1] = 2
indegree[2] = 1
indegree[3] = 1
indegree[4] = 0
indegree[5] = 0
Kahn 算法实现
Kahn 算法的核心步骤如下:
- 计算所有顶点的入度
- 将所有入度为 0 的顶点加入队列
- 当队列不为空时,取出队首顶点
u,将其加入拓扑序列 - 遍历
u的所有邻居v,将v的入度减 1;若v的入度变为 0,则将v入队 - 重复步骤 3-4 直到队列为空
- 如果拓扑序列中的顶点数等于总顶点数,排序成功;否则图中存在环
以示例图跟踪 Kahn 算法的执行过程:
初始: indegree = [2, 2, 1, 1, 0, 0]
入度为0的顶点: 4, 5 → 入队
步骤1: 出队4, 输出4
邻居0: indegree[0] = 2-1 = 1
邻居1: indegree[1] = 2-1 = 1
队列: [5]
步骤2: 出队5, 输出5
邻居2: indegree[2] = 1-1 = 0 → 入队
邻居0: indegree[0] = 1-1 = 0 → 入队
队列: [2, 0]
步骤3: 出队2, 输出2
邻居3: indegree[3] = 1-1 = 0 → 入队
队列: [0, 3]
步骤4: 出队0, 输出0
0无出边
队列: [3]
步骤5: 出队3, 输出3
邻居1: indegree[1] = 1-1 = 0 → 入队
队列: [1]
步骤6: 出队1, 输出1
1无出边
队列: []
拓扑排序结果: 4 5 2 0 3 1
已处理6个顶点 = 总顶点数6, 无环
C++ 实现
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
// Topological sort using Kahn's algorithm (indegree-based)
vector<int> topologicalSort(const vector<vector<int>>& adj) {
int n = adj.size();
vector<int> indegree(n, 0);
// Step 1: Compute indegree for each vertex
for (int u = 0; u < n; u++) {
for (int v : adj[u]) {
indegree[v]++;
}
}
// Step 2: Enqueue all vertices with indegree 0
queue<int> q;
for (int i = 0; i < n; i++) {
if (indegree[i] == 0) {
q.push(i);
}
}
// Step 3-5: Process vertices
vector<int> result;
while (!q.empty()) {
int u = q.front();
q.pop();
result.push_back(u);
// Reduce indegree of neighbors
for (int v : adj[u]) {
indegree[v]--;
if (indegree[v] == 0) {
q.push(v);
}
}
}
// Step 6: Check for cycle
if ((int)result.size() != n) {
cout << "Graph contains a cycle! Topological sort not possible." << endl;
return {};
}
return result;
}
int main() {
int n = 6;
vector<vector<int>> adj(n);
adj[5].push_back(2);
adj[5].push_back(0);
adj[4].push_back(0);
adj[4].push_back(1);
adj[2].push_back(3);
adj[3].push_back(1);
vector<int> order = topologicalSort(adj);
if (!order.empty()) {
cout << "Topological Sort: ";
for (int v : order) {
cout << v << " ";
}
cout << endl;
}
return 0;
}
C 实现
#include <stdio.h>
#include <stdbool.h>
#define MAX_NODES 6
#define MAX_NEIGHBORS 4
// Simple queue using circular array
typedef struct {
int data[MAX_NODES];
int front, rear;
} Queue;
void queueInit(Queue* q) { q->front = 0; q->rear = 0; }
bool queueEmpty(Queue* q) { return q->front == q->rear; }
void queuePush(Queue* q, int val) { q->data[q->rear++] = val; }
int queuePop(Queue* q) { return q->data[q->front++]; }
// Topological sort using Kahn's algorithm (indegree-based)
void topologicalSort(int adj[][MAX_NEIGHBORS], int adjCount[], int n) {
int indegree[MAX_NODES] = {0};
int result[MAX_NODES];
int resultLen = 0;
int i, j;
// Step 1: Compute indegree for each vertex
for (i = 0; i < n; i++) {
for (j = 0; j < adjCount[i]; j++) {
indegree[adj[i][j]]++;
}
}
// Step 2: Enqueue all vertices with indegree 0
Queue q;
queueInit(&q);
for (i = 0; i < n; i++) {
if (indegree[i] == 0) {
queuePush(&q, i);
}
}
// Step 3-5: Process vertices
while (!queueEmpty(&q)) {
int u = queuePop(&q);
result[resultLen++] = u;
// Reduce indegree of neighbors
for (j = 0; j < adjCount[u]; j++) {
int v = adj[u][j];
indegree[v]--;
if (indegree[v] == 0) {
queuePush(&q, v);
}
}
}
// Step 6: Check for cycle
if (resultLen != n) {
printf("Graph contains a cycle! Topological sort not possible.\n");
return;
}
// Print result
printf("Topological Sort: ");
for (i = 0; i < resultLen; i++) {
printf("%d ", result[i]);
}
printf("\n");
}
int main() {
int adj[MAX_NODES][MAX_NEIGHBORS] = {0};
int adjCount[MAX_NODES] = {0};
#define ADD_EDGE(u, v) do { \
adj[u][adjCount[u]++] = v; \
} while(0)
ADD_EDGE(5, 2);
ADD_EDGE(5, 0);
ADD_EDGE(4, 0);
ADD_EDGE(4, 1);
ADD_EDGE(2, 3);
ADD_EDGE(3, 1);
topologicalSort(adj, adjCount, 6);
return 0;
}
Python 实现
from collections import deque
def topological_sort(adj):
"""Topological sort using Kahn's algorithm (indegree-based)."""
n = len(adj)
indegree = [0] * n
# Step 1: Compute indegree for each vertex
for u in range(n):
for v in adj[u]:
indegree[v] += 1
# Step 2: Enqueue all vertices with indegree 0
q = deque()
for i in range(n):
if indegree[i] == 0:
q.append(i)
# Step 3-5: Process vertices
result = []
while q:
u = q.popleft()
result.append(u)
# Reduce indegree of neighbors
for v in adj[u]:
indegree[v] -= 1
if indegree[v] == 0:
q.append(v)
# Step 6: Check for cycle
if len(result) != n:
print("Graph contains a cycle! Topological sort not possible.")
return []
return result
if __name__ == "__main__":
adj = {i: [] for i in range(6)}
adj[5] = [2, 0]
adj[4] = [0, 1]
adj[2] = [3]
adj[3] = [1]
order = topological_sort(adj)
if order:
print("Topological Sort:", " ".join(map(str, order)))
Go 实现
package main
import "fmt"
// Topological sort using Kahn's algorithm (indegree-based)
func topologicalSort(adj [][]int) []int {
n := len(adj)
indegree := make([]int, n)
// Step 1: Compute indegree for each vertex
for u := 0; u < n; u++ {
for _, v := range adj[u] {
indegree[v]++
}
}
// Step 2: Enqueue all vertices with indegree 0
queue := []int{}
for i := 0; i < n; i++ {
if indegree[i] == 0 {
queue = append(queue, i)
}
}
// Step 3-5: Process vertices
var result []int
for len(queue) > 0 {
u := queue[0]
queue = queue[1:]
result = append(result, u)
// Reduce indegree of neighbors
for _, v := range adj[u] {
indegree[v]--
if indegree[v] == 0 {
queue = append(queue, v)
}
}
}
// Step 6: Check for cycle
if len(result) != n {
fmt.Println("Graph contains a cycle! Topological sort not possible.")
return nil
}
return result
}
func main() {
adj := make([][]int, 6)
adj[5] = append(adj[5], 2, 0)
adj[4] = append(adj[4], 0, 1)
adj[2] = append(adj[2], 3)
adj[3] = append(adj[3], 1)
order := topologicalSort(adj)
if order != nil {
fmt.Print("Topological Sort: ")
for i, v := range order {
if i > 0 {
fmt.Print(" ")
}
fmt.Print(v)
}
fmt.Println()
}
}
上述代码实现了基于入度数组的 Kahn 拓扑排序算法。四种语言的实现逻辑完全一致:首先计算所有顶点的入度,然后将入度为 0 的顶点入队;接着不断取出队首顶点加入结果序列,并将其邻居的入度减 1,若邻居入度变为 0 则入队。最后检查结果序列的长度是否等于顶点总数,若不等则说明图中存在环,无法进行拓扑排序。C++ 使用 STL 的 queue;C 语言手动实现了基于数组的队列;Python 使用 collections.deque;Go 使用切片模拟队列。
运行该程序将输出:
Topological Sort: 4 5 2 0 3 1
拓扑排序的性质
下表总结了 Kahn 算法的时间和空间复杂度:
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度(Time Complexity) | O(V + E) | 计算入度 O(V + E),每个顶点最多入队一次 O(V),每条边最多被处理一次 O(E) |
| 空间复杂度(Space Complexity) | O(V + E) | 邻接表 O(V + E),入度数组 O(V),队列 O(V) |
其中 V 是顶点数(Vertex Count),E 是边数(Edge Count)。
拓扑排序的关键性质:
- 前提条件:拓扑排序仅适用于有向无环图(DAG)。如果图中存在环,则不存在合法的拓扑序列。Kahn 算法通过比较已处理顶点数与总顶点数来检测环。
- 不唯一性:一个 DAG 的拓扑排序序列不一定是唯一的。当同时存在多个入度为 0 的顶点时,选择不同的处理顺序会产生不同的拓扑序列。但所有合法的拓扑序列都满足边的先后约束。
- 环检测(Cycle Detection):Kahn 算法天然地能检测有向图中的环。如果最终结果序列中的顶点数小于总顶点数,说明有部分顶点始终无法达到入度为 0 的状态——它们形成了环。
- 与 DFS 的关系:拓扑排序也可以通过深度优先搜索(DFS)实现——按完成时间的逆序排列顶点。Kahn 算法(BFS 风格)和 DFS 方法的时间复杂度都是 O(V + E),但 Kahn 算法能更自然地检测环。
拓扑排序的典型应用场景:
| 应用场景 | 说明 |
|---|---|
| 任务调度(Task Scheduling) | 确定有依赖关系的任务的执行顺序 |
| 编译依赖(Build System) | Make 等构建工具确定编译顺序 |
| 课程安排(Course Scheduling) | 根据先修课要求安排选课顺序 |
| 电子表格(Spreadsheet) | 确定单元格的计算顺序 |
| 数据处理管道(Data Pipeline) | 确定 ETL 任务的执行顺序 |
| 版本控制(Version Control) | 确定合并操作的先后顺序 |

浙公网安备 33010602011771号