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 算法的核心步骤如下:

  1. 计算所有顶点的入度
  2. 将所有入度为 0 的顶点加入队列
  3. 当队列不为空时,取出队首顶点 u,将其加入拓扑序列
  4. 遍历 u 的所有邻居 v,将 v 的入度减 1;若 v 的入度变为 0,则将 v 入队
  5. 重复步骤 3-4 直到队列为空
  6. 如果拓扑序列中的顶点数等于总顶点数,排序成功;否则图中存在环

以示例图跟踪 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) 确定合并操作的先后顺序
posted @ 2026-04-17 07:57  游翔  阅读(14)  评论(0)    收藏  举报