【算法与数据结构】拓扑排序实战(栈+邻接表+环判断,附可运行代码)

拓扑排序实战!教材P301图8.43手把手通关(栈+邻接表+环判断,附可运行代码)

刚做拓扑排序实验时,我对着屏幕卡了俩小时:一是教材P301图8.43的7个顶点关系理不清,不知道"1→3""2→7"这些边怎么对应代码;二是跑原文代码时,因为少写个逗号(n m写成nm)编译报错,还没加环判断,明明图没问题却输出不全。后来才发现,拓扑排序的核心就像"选课"------先选无先修课的(入度0),选完解锁后续课(入度减1),用栈存"能选的课"就行。

今天就把这份能直接跑通的教程拆透,从教材图的结构可视化,到代码逐行注释,再到一步步走流程,新手也能复现"1 2 3 4 5 7 6"的正确序列,还会指出实验必踩的坑。

一、先看懂:实验用的图长啥样?(教材P301图8.43)

实验的核心是7个顶点(编号1-7)、8条边的有向图 ,可以理解为"简化选课系统":顶点是课程,有向边i→j表示"选j课前必须先选i"。先把边和入度(先修课数量)列清楚,后续所有操作都围绕它展开:

有向边 选课逻辑(j的先修课是i) 顶点初始入度(先修课数量)
1→3 选3课前先选1 1:0;3:1
2→4、2→5、2→7 选4/5/7前先选2 2:0;4:2;5:2;7:1
3→4 选4前也能先选3 4:2(先修2和3)
4→5、4→6 选5/6前先选4 5:2(先修2和4);6:2
7→6 选6前先选7 6:2(先修4和7)

简单说:入度为0的顶点(1、2)是"无先修课的课",能优先选;选完后减少后续课程的入度,入度变0就能继续选。

二、核心原理:3句话讲透拓扑排序

不用记复杂定义,用大白话理解核心:

  1. 拓扑序列 :比如"1 2 3 4 5 7 6",所有边i→j都满足"i在j前面"(先选i,再选j),符合选课逻辑;
  2. 入度:顶点的"先修课数量",入度=0 → 无先修,能优先处理;
  3. 算法逻辑:用栈存"入度0的顶点"(能选的课),处理后解锁后续顶点(入度-1),重复到所有顶点输出(无环)或栈空(有环)。

三、完整可运行代码

c 复制代码
#include<stdio.h>
#include<stdlib.h>
#define MaxSize 10  // 足够存7个顶点

// 1. 边节点(邻接表的"边":存邻接顶点+下一条边)
typedef struct EdgeNode {
    int adjvex;          // 邻接顶点编号(比如1的邻接顶点是3)
    struct EdgeNode *next; // 下一条边的指针
} EdgeNode;

// 2. 顶点节点(邻接表的"顶点":存编号+入度+第一条边)
typedef struct VertexNode {
    int vertex;          // 顶点编号(1-7)
    int in;              // 入度(先修课数量)
    EdgeNode *first;     // 指向第一条边的指针
} VertexNode;

// 3. 图结构(邻接表)
typedef struct {
    VertexNode adjlist[MaxSize];  // 所有顶点的数组
    int vertexNum;                // 顶点总数(实验中7)
    int edgeNum;                  // 边总数(实验中8)
} ALGraph;

// 4. 创建图:用头插法建邻接表(输入边的关系)
ALGraph CreatGraph(ALGraph G, int n, int m) {
    int i, a, b;          // a→b:有向边
    EdgeNode *s = NULL;   // 临时边节点

    G.vertexNum = n;      // 赋值顶点数
    G.edgeNum = m;        // 赋值边数

    // 初始化每个顶点:编号、入度0、无第一条边
    for (i = 1; i <= G.vertexNum; i++) {
        G.adjlist[i].vertex = i;
        G.adjlist[i].in = 0;
        G.adjlist[i].first = NULL;
    }

    // 输入每条边,头插法插入邻接表
    for (i = 1; i <= G.edgeNum; i++) {
        printf("第%d条边(a→b):", i);
        scanf("%d %d", &a, &b);

        // ① 更新b的入度(b多一个先修课a)
        G.adjlist[b].in++;

        // ② 创建边节点,插入a的邻接表(头插法:新边在最前面)
        s = (EdgeNode*)malloc(sizeof(EdgeNode));
        s->adjvex = b;          // 边指向b
        s->next = G.adjlist[a].first;  // 新边接在原有边前面
        G.adjlist[a].first = s;       // a的第一条边指向新边
    }
    return G;
}

// 5. 核心:栈实现拓扑排序(含环判断)
void TopSort(ALGraph G) {
    int i, j, k, count = 0;  // count:已输出的顶点数
    int S[MaxSize], top = 0; // 栈S:存放入度0的顶点,top=栈顶指针
    EdgeNode *p = NULL;      // 遍历边的指针

    // 第一步:初始化栈:入度0的顶点压栈(能先选的课)
    for (i = 1; i <= G.vertexNum; i++) {
        if (G.adjlist[i].in == 0) {
            S[++top] = i;  // 栈从1开始存(top初始0,++top后是1)
        }
    }

    // 第二步:处理栈中顶点,直到栈空
    while (top != 0) {
        // ① 弹出栈顶顶点(选当前能选的课)
        j = S[top--];
        printf("%d ", G.adjlist[j].vertex);  // 输出顶点
        count++;                              // 已输出数+1

        // ② 处理该顶点的所有邻接边(解锁后续课)
        p = G.adjlist[j].first;  // 取j的第一条边
        while (p != NULL) {
            k = p->adjvex;       // 邻接顶点k(j→k)
            G.adjlist[k].in--;   // k的入度-1(少一个先修课)

            // ③ 若k的入度变0(先修课全满足),压栈
            if (G.adjlist[k].in == 0) {
                S[++top] = k;
            }
            p = p->next;  // 遍历下一条边
        }
    }

    // 第三步:判断是否有环(输出数<总顶点数→有环)
    if (count < G.vertexNum) {
        printf("\n⚠️  有向图存在环,无法拓扑排序!");
    }
}

// 主函数:测试教材P301图8.43(7顶点8边)
int main() {
    ALGraph G;
    int n = 7, m = 8;  // 实验图参数:7个顶点,8条边

    // 1. 创建图(输入教材图的8条边:1 3;2 4;2 5;2 7;3 4;4 5;4 6;7 6)
    printf("=== 教材P301图8.43(7顶点8边)===\n");
    G = CreatGraph(G, n, m);  // 修正原文:n和m之间加逗号

    // 2. 拓扑排序
    printf("\n拓扑排序结果:");
    TopSort(G);

    return 0;
}




四、流程拆解:用教材例子一步步走(新手必看)

以教材图8.43为例,输入边为1 3;2 4;2 5;2 7;3 4;4 5;4 6;7 6,一步步看栈和入度的变化,理解每步逻辑:

步骤1:初始化栈(入度0的顶点压栈)

  • 初始入度0的顶点:1(入度0)、2(入度0);
  • 栈S:[1, 2](top=2);count=0。

步骤2:弹出栈顶顶点2,处理邻接边

  • 弹出2(top=1),输出"2 ",count=1;
  • 2的邻接顶点:4、5、7 → 入度各减1:
    • 4的入度:2→1;5的入度:2→1;7的入度:1→0;
  • 7的入度变0,压栈 → 栈S:[1, 7](top=2)。

步骤3:弹出栈顶顶点7,处理邻接边

  • 弹出7(top=1),输出"2 7 ",count=2;
  • 7的邻接顶点:6 → 6的入度:2→1;
  • 6的入度未到0,不压栈 → 栈S:[1](top=1)。

步骤4:弹出栈顶顶点1,处理邻接边

  • 弹出1(top=0),输出"2 7 1 ",count=3;
  • 1的邻接顶点:3 → 3的入度:1→0;
  • 3的入度变0,压栈 → 栈S:[3](top=1)。

步骤5:弹出栈顶顶点3,处理邻接边

  • 弹出3(top=0),输出"2 7 1 3 ",count=4;
  • 3的邻接顶点:4 → 4的入度:1→0;
  • 4的入度变0,压栈 → 栈S:[4](top=1)。

步骤6:弹出栈顶顶点4,处理邻接边

  • 弹出4(top=0),输出"2 7 1 3 4 ",count=5;
  • 4的邻接顶点:5、6 → 入度各减1:
    • 5的入度:1→0;6的入度:1→0;
  • 5和6压栈 → 栈S:[5, 6](top=2)。

步骤7:弹出栈顶顶点6,处理邻接边

  • 弹出6(top=1),输出"2 7 1 3 4 6 ",count=6;
  • 6无邻接边 → 栈S:[5](top=1)。

步骤8:弹出栈顶顶点5,处理邻接边

  • 弹出5(top=0),输出"2 7 1 3 4 6 5 ",count=7;
  • 5无邻接边 → 栈空(top=0)。

最终结果

输出序列可能是"2 7 1 3 4 6 5"(拓扑序列不唯一,原文的"1 2 3 4 5 7 6"也正确,只要满足边的前后关系即可),count=7等于顶点数,无环。

五、新手避坑点(我踩过的坑,你别踩)

  1. 语法错误:CreatGraph调用少逗号
    原文G=CreatGraph(G, n m) → 修正为G=CreatGraph(G, n, m),否则编译报错"语法错误";
  2. 入度更新遗漏
    处理邻接顶点时忘记G.adjlist[k].in--,导致入度永远不为0,栈空后count<顶点数,误判有环;
  3. 没有环的判断
    原文没写if (count < G.vertexNum),无法检测环(比如图中有1→2、2→3、3→1,会输出部分顶点后停止,却不提示有环);
  4. 栈的初始化错误
    S[++top]=i写成S[top++]=i,导致栈底元素存错,后续弹出顺序混乱。

六、总结:拓扑排序的实际价值

拓扑排序不只是实验题,在实际场景中超有用:

  • 选课系统:先选无先修课的课程,解锁后续课程;
  • 任务调度:项目开发中,先做"无前置任务"的模块(如先搭框架,再写功能);
  • 编译依赖:编译器先编译无依赖的文件,再编译依赖该文件的代码。

通过这个实验,不仅掌握了"入度+栈"实现拓扑排序的方法,还理解了邻接表在图存储中的优势------相比邻接矩阵,稀疏图用邻接表更省空间。

相关推荐
Theodore_10223 小时前
机器学习(6)特征工程与多项式回归
深度学习·算法·机器学习·数据分析·多项式回归
lingling0094 小时前
机械臂动作捕捉系统选型指南:从需求到方案,NOKOV 度量光学动捕成优选
人工智能·算法
吃着火锅x唱着歌4 小时前
LeetCode 410.分割数组的最大值
数据结构·算法·leetcode
Benny_Tang4 小时前
题解:P7989 [USACO21DEC] Bracelet Crossings G
c++·算法
YSRM4 小时前
Leetcode+Java+图论+并查集
算法·leetcode·图论
康谋自动驾驶6 小时前
拆解3D Gaussian Splatting:原理框架、实战 demo 与自驾仿真落地探索!
算法·数学建模·3d·自动驾驶·汽车
AI科技星6 小时前
垂直原理:宇宙的沉默法则与万物运动的终极源头
android·服务器·数据结构·数据库·人工智能
QuantumLeap丶7 小时前
《数据结构:从0到1》-05-数组
数据结构·数学