C++ 实现图的拓扑排序:原理、算法与代码示例

程一诺
程一诺
发布时间:2026-10-15| 5人浏览过
原创

拓扑排序是处理有向无环图(DAG)线性化问题的核心算法。本文将深入解析其逻辑原理,重点介绍基于入度计算的 Kahn 算法在 C++ 中的具体实现,包括邻接表存储结构、入度数组维护以及队列操作,并提供可直接用于解题的标准代码模板。

拓扑排序的核心概念

拓扑排序是针对有向图的一种顶点线性排序方法。一个合法的拓扑序列必须满足两个条件:首先,图中的每个顶点在序列中仅出现一次;其次,若图中存在从顶点 x 到顶点 y 的有向边,则在该序列中 x 必须出现在 y 之前。因此,拓扑排序仅适用于有向无环图(DAG)。如果图中存在环路,则无法找到有效的拓扑序。

理解拓扑排序的关键在于“入度”与“出度”。入度指指向当前节点的边的数量,即有多少条边指向自己;出度指从当前节点发出的边的数量,即有多少条边从自己指出。拓扑排序的本质就是不断寻找入度为 0 的节点,将其加入序列后“移除”,从而降低其 successor 节点的入度,直至所有节点都被排序。

Kahn 算法流程与逻辑

Kahn 算法是实现拓扑排序最直观的方法,主要依赖队列来执行。算法的执行步骤如下:

  1. 初始化:计算图中所有节点的入度,并将所有入度为 0 的节点加入队列。
  2. 循环处理:只要队列不为空,就取出队首节点 t,将其加入拓扑序列。随后,遍历以 t 为起点的的所有边,将 t 指向的节点 j 的入度减 1。如果节点 j 的入度变为 0,则将 j 加入队列。
  3. 结束判断:当队列空时,检查输出的顶点数是否等于图中总顶点数 n。若小于 n,说明图中存在回路,无法拓扑排序;否则,输出的序列即为一个拓扑序(注意:拓扑序可能不唯一)。

该算法的时间复杂度为 O(n + m),其中 n 为点数,m 为边数,主要耗时在于遍历所有节点和边。

C++ 代码实现

在实际编程或算法竞赛中,通常使用邻接表来存储稀疏图,并利用数组模拟链表以提高效率。以下提供两种常见的 C++ 实现方式。

第1步:基础模板实现(STL queue)

这是最标准的实现方式,逻辑清晰,易于理解。首先定义图的存储结构(邻接表),并维护一个入度数组 in[]。

#include <iostream>
#include <queue>
using namespace std;

const int N = 1e5 + 10;
int h[N], e[N], ne[N], idx; // 邻接表头指针、边值、下一个节点索引、下标
int in[N]; // 入度数组
int n, m;

void add(int a, int b) {
    e[idx] = b; ne[idx] = h[a]; h[a] = idx++;
}

bool topsort() {
    queue<int> q;
    // 1. 将所有入度为 0 的点加入队列
    for (int i = 1; i <= n; i++) {
        if (in[i] == 0)
            q.push(i);
    }

    int cnt = 0; // 记录拓扑序列中节点的个数
    while (!q.empty()) {
        int t = q.front();
        q.pop();
        cnt++; // 找到一个入度为0的点,计数加1

        // 2. 删除从t出发的所有边,即减小后继节点的入度
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            in[j]--; // 入度减1
            if (in[j] == 0) {
                q.push(j); // 如果入度变为0,加入队列
            }
        }
    }
    return cnt == n; // 如果处理的节点数等于总节点数,则无环
}

int main() {
    cin >> n >> m;
    memset(h, -1, sizeof h); // 初始化邻接表头指针
    for (int i = 0; i < m; i++) {
        int a, b;
        cin >> a >> b;
        add(a, b);
        in[b]++; // 记录b的入度增加
    }

    if (topsort()) {
        cout << "Topological sort exists." << endl;
        // 若要输出具体序列,可在topsot过程中记录
    } else {
        cout << "-1" << endl; // 存在环
    }
    return 0;
}

第2步:数组模拟队列优化实现

对于数据规模较大或对性能有严格要求的场景,可以使用数组模拟队列以减少 STL 容器的开销。这种方法通常配合静态分配的 top[] 数组直接存储结果。

// 假设全局变量 h, e, ne, idx, in[], n 已定义
// top[] 用于存储拓扑序列
bool topsort_optimized() {
    int hh = 0, tt = -1; // 队头和队尾指针
    
    // 1. 初始化队列:放入所有入度为0的点
    for (int i = 1; i <= n; i++) {
        if (in[i] == 0) {
            top[++tt] = i;
        }
    }
    
    while (hh <= tt) {
        int t = top[hh++]; // 取出队头元素
        
        // 2. 遍历 t 的出边,更新后继节点入度
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            in[j]--; // 入度减1
            if (in[j] == 0) {
                top[++tt] = j; // 入度为0,加入队列末尾
            }
        }
    }
    
    // 如果所有点都入队了(tt == n-1,因为从0开始编号存储且总数为n),说明无环
    return tt == n - 1;
}

应用场景与注意事项

  • 任务调度:在有依赖关系的任务系统中(如编译项目、课程选修),拓扑排序可用于确定任务的执行顺序。例如,必修课必须在选修课之前完成。
  • 环检测:如果拓扑排序结束后输出的节点数少于图的总节点数,说明图中存在环,这在死锁检测或版本冲突检查中非常有用。
  • 重边与自环:上述代码标准实现中,邻接表天然支持重边处理(每条边独立记录),但自环会导致该节点入度永远无法降为 0,从而被正确识别为环。

总结

C++ 实现图的拓扑排序主要依赖 Kahn 算法,核心在于维护节点入度和队列操作。通过邻接表存储图结构,可以高效地遍历和处理边关系。掌握数组模拟队列与 STL 库的两种写法,能够应对不同场景下的性能需求。以上就是 C++ 详细讲解图的拓扑排序的详细内容,更多关于 C++ 图算法的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。