拓扑排序实战!教材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 2 3 4 5 7 6",所有边
i→j
都满足"i在j前面"(先选i,再选j),符合选课逻辑; - 入度:顶点的"先修课数量",入度=0 → 无先修,能优先处理;
- 算法逻辑:用栈存"入度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等于顶点数,无环。
五、新手避坑点(我踩过的坑,你别踩)
- 语法错误:CreatGraph调用少逗号
原文G=CreatGraph(G, n m)
→ 修正为G=CreatGraph(G, n, m)
,否则编译报错"语法错误"; - 入度更新遗漏
处理邻接顶点时忘记G.adjlist[k].in--
,导致入度永远不为0,栈空后count<顶点数,误判有环; - 没有环的判断
原文没写if (count < G.vertexNum)
,无法检测环(比如图中有1→2、2→3、3→1,会输出部分顶点后停止,却不提示有环); - 栈的初始化错误
把S[++top]=i
写成S[top++]=i
,导致栈底元素存错,后续弹出顺序混乱。
六、总结:拓扑排序的实际价值
拓扑排序不只是实验题,在实际场景中超有用:
- 选课系统:先选无先修课的课程,解锁后续课程;
- 任务调度:项目开发中,先做"无前置任务"的模块(如先搭框架,再写功能);
- 编译依赖:编译器先编译无依赖的文件,再编译依赖该文件的代码。
通过这个实验,不仅掌握了"入度+栈"实现拓扑排序的方法,还理解了邻接表在图存储中的优势------相比邻接矩阵,稀疏图用邻接表更省空间。