拓扑排序(Topological Sort)—— C语言链式栈+邻接表实现详解
一、什么是拓扑排序?
拓扑排序是对 有向无环图(DAG) 的顶点进行线性排序,使得对于每一条有向边 u → v,顶点 u 都排在顶点 v 的前面。
应用场景:
- 课程安排(先修课关系)
- 工程编译依赖
- 任务调度
拓扑排序只存在于有向无环图中。若图中存在环,则无法进行拓扑排序。
二、Kahn 算法思想
Kahn 算法(卡恩算法)是拓扑排序的经典算法,核心思想:
- 统计入度:遍历所有边,计算每个顶点的入度(有多少条边指向它)。
- 入度为 0 的入队/入栈:将入度为 0 的顶点加入容器(可以是队列或栈)。
- 取出顶点:从容器中取出一个顶点,加入拓扑序列。
- 更新入度:将该顶点的所有出边邻接点的入度减 1,如果某个邻接点的入度变为 0,则将其加入容器。
- 重复步骤 3-4,直到所有顶点都处理完。
如何判断有环? 如果处理完的顶点数 < 总顶点数,说明图中存在环。
三、代码逐层解析
3.1 链式栈的实现
typedef struct stackNode {
int data;
struct stackNode* next;
} sNode, *Stack;
使用带头结点的单链表实现栈,栈顶是头结点的下一个结点。
| 操作 | 说明 |
|---|---|
InitStack() | 创建空栈(头结点) |
Push(s, k) | 头插法插入元素 |
Pop(s) | 删除头结点后的第一个元素 |
GetTop(s) | 返回栈顶元素 |
IsEmpty(s) | 判断栈是否为空 |
3.2 图的邻接表存储
struct Graph {
char data; // 顶点名称(如 A, B, C...)
ENode* first; // 出边链表头指针
} g[105];
g[i]表示第 i 个顶点ENode是出边链表的结点,adj存储邻接点的下标ind[]数组记录每个顶点的入度topo[]数组记录最终的拓扑序列
3.3 Find() 函数
int Find(char x) {
for (int i = 1; i <= n; i++)
if (g[i].data == x) break;
return i;
}
根据顶点字符名称查找其在数组中的下标。时间复杂度 O(n),图不大时够用。
3.4 建图过程
for (int i = 1; i <= m; i++) {
scanf(" %c %c", &x, &y);
xi = Find(x); yi = Find(y);
ind[yi]++; // y 的入度 +1
// 头插法将 y 插入 x 的出边链表
ENode* e = (ENode*)malloc(sizeof(ENode));
e->adj = yi;
e->next = g[xi].first;
g[xi].first = e;
}
对每条有向边 x → y:
y的入度ind[yi]++- 在
x的出边链表中头插一个结点,值为y的下标
3.5 核心:TopoSort()
void TopoSort() {
Stack s = InitStack();
// 第一轮:把所有入度为 0 的点入栈
for (int i = 1; i <= n; i++)
if (ind[i] == 0) s = Push(s, i);
for (int i = 1; i <= n; i++) {
if (IsEmpty(s)) break; // 栈空 → 有环,提前退出
int j = GetTop(s); s = Pop(s);
topo[++k] = g[j].data; // 加入拓扑序列
ENode* p = g[j].first;
while (p != NULL) {
ind[p->adj]--; // 邻接点入度 -1
if (ind[p->adj] == 0)
s = Push(s, p->adj);
p = p->next;
}
}
}
核心逻辑:
- 初始化栈,将入度为 0 的顶点全部入栈
- 每次循环弹出一个栈顶顶点,加入拓扑序列
- 遍历该顶点的所有出边,将邻接点入度减 1
- 如果邻接点入度变为 0,则入栈
- 若过程中栈变空但顶点还没处理完,说明有环
这里用栈而不是队列,得到的拓扑序可能与用队列不同(但都是合法的拓扑序)。
3.6 主函数与测试
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf(" %c", &g[i].data);
g[i].first = NULL;
}
// ... 读入 m 条边 ...
TopoSort();
if (k != n)
printf("有环\n");
else
for (int i = 1; i <= n; i++)
printf("%c ", topo[i]);
return 0;
}
四、测试样例
6 8
A B C D E F
A B
A C
A D
C B
C E
F D
F E
D E
图的结构:
A → B, C, D
B → (无出边)
C → B, E
D → E
F → D, E
运行结果:
A F C B D E
或
F A C B D E
(取决于入度为0的顶点的入栈顺序)
五、算法复杂度分析
| 维度 | 复杂度 |
|---|---|
| 时间复杂度 | O(n + m),n 个顶点,m 条边,每个顶点入栈一次,每条边被遍历一次 |
| 空间复杂度 | O(n),栈 + 入度数组 + 拓扑序列数组 |
六、总结
| 要点 | 说明 |
|---|---|
| 适用范围 | 有向无环图 |
| 核心数据结构 | 邻接表 + 栈/队列 |
| 判断有环 | 最终拓扑序列长度 < 顶点总数 |
| 入度思想 | 每次去掉"没有前置依赖"的顶点 |
拓扑排序的关键在于入度这个概念的运用——入度为 0 意味着"没有前置约束",可以放心地加入结果序列。Kahn 算法直观且高效,是图论入门的必学算法之一。