一、拓扑排序基础概念
1. 什么是拓扑排序?
拓扑排序是对有向无环图(DAG)的一种线性排序,满足:
(1)对于图中的每一条有向边(u → v),u在排序中总是位于v的前面
(2)如果图中存在环,则无法进行拓扑排序
2. 典型应用场景
(1)课程安排:确定课程学习顺序(先修课程在前)
(2)任务调度:确定任务执行顺序(依赖任务在前)
(3)软件构建:确定编译顺序(依赖模块在前)
(4)电子表格:确定计算公式求值顺序
二、算法实现详解
1. 数据结构准备
#define MAX 100
typedef struct Node {
int vertex;
struct Node* next;
} Node;
typedef struct Graph {
Node* adjLists[MAX]; // 邻接表
int inDegree[MAX]; // 入度数组
int numVertices; // 顶点数
} Graph;
2. 关键算法步骤(Kahn算法/BFS方法)
算法流程:
-
初始化:计算所有顶点的入度
-
队列准备:将所有入度为0的顶点加入队列
-
处理队列:
-
取出队首顶点u,加入结果
-
对u的每个邻接顶点v:
-
将v的入度减1
-
如果v的入度变为0,加入队列
-
-
-
结果检查:如果结果包含所有顶点,则成功;否则有环
3. 完整C语言实现
bool topologicalSort(Graph* graph, int* result) {
int queue[MAX], front = 0, rear = 0;
int count = 0;
int* inDegree = malloc(graph->numVertices * sizeof(int));
// 复制入度数组
for (int i = 0; i < graph->numVertices; i++) {
inDegree[i] = graph->inDegree[i];
if (inDegree[i] == 0) {
queue[rear++] = i; // 入队
}
}
while (front < rear) {
int u = queue[front++];
result[count++] = u;
Node* temp = graph->adjLists[u];
while (temp != NULL) {
int v = temp->vertex;
if (--inDegree[v] == 0) {
queue[rear++] = v;
}
temp = temp->next;
}
}
free(inDegree);
return count == graph->numVertices;
}
三、应用实例解析
1.课程安排问题
210. 课程表 II - 力扣(LeetCode)
现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。
例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1]
示例 2:
输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] 输出:[0,2,1,3] 解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。 因此,一个正确的课程顺序是[0,1,2,3]另一个正确的排序是[0,2,1,3]
示例 3:
输入:numCourses = 1, prerequisites = [] 输出:[0]
class Solution {
public:
//存储有向图
vector<vector<int>> edges;
//存储每个节点的入度
vector<int> inde;
//存储结果
vector<int> res;
vector<int> findOrder(int n, vector<vector<int>>& p) {
edges.resize(n);
inde.resize(n);
for(int i=0;i<p.size();i++){
edges[p[i][1]].push_back(p[i][0]);
inde[p[i][0]]++;}
queue<int> q;
for(int i=0;i<n;i++){
if(inde[i]==0) q.push(i);}
while(!q.empty()){
int x=q.front();
q.pop();
res.push_back(x);
for(int y:edges[x]){
if(--inde[y]==0) q.push(y);}
}
if(res.size()!=n) return {};
return res;
}
};
2. 工程任务调度实例
场景:
任务A:设计数据库
任务B:开发后端API(依赖A)
任务C:开发前端界面(依赖B)
任务D:编写文档(依赖A)
可能的拓扑排序结果:
A → B → C → D
A → D → B → C
四、复杂度与优化
1. 复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 构建图 | O(E) | O(V+E) |
| 拓扑排序 | O(V+E) | O(V) |
2. 性能优化建议
(1)动态数组:使用动态数组代替固定大小数组
(2)内存池:为节点分配使用内存池技术
(3)并行处理:对于大型图可考虑并行计算入度
五、常见问题解答
1.如何处理图中存在环的情况?
当算法结束时,如果排序结果中的顶点数少于图中总顶点数,则说明图中存在环。
2.为什么拓扑排序结果不唯一?
因为可能有多个顶点在同一"层级",处理顺序可以不同。
六、扩展学习
-
关键路径算法:在拓扑排序基础上计算项目最短完成时间
-
强连通分量:Kosaraju算法需要反向图的拓扑排序
-
动态拓扑排序:处理动态变化的图结构
8440

被折叠的 条评论
为什么被折叠?



