拓扑排序算法详解:定义、实现与应用场景
创作时间:
作者:
@小白创作中心
拓扑排序算法详解:定义、实现与应用场景
引用
CSDN
1.
https://m.blog.csdn.net/2401_82677021/article/details/145566900
拓扑排序是图论中的一个重要算法,主要用于处理有向无环图(DAG)的线性排序问题。它在任务调度、课程安排、编译器优化等多个领域都有广泛的应用。本文将详细介绍拓扑排序的定义、实现方法以及应用场景,帮助读者全面理解这一算法。
定义
拓扑排序是针对有向无环图(DAG,Directed Acyclic Graph)的一种线性排序算法。其目标是使得对于图中的每一条有向边u->v,节点u在排序中都出现在节点v之前。
对于这个有向无环图,边有1->2,2->3,1->4,4->5,2->3。那么在拓扑排序中,1一定出现在2的前面,2一定出现在3的前面......。
拓扑排序的过程:每次选数时,都选择图中入度为0的节点,然后遍历该节点所连接的节点,将它们的入度减一,重复该过程,直到排序结果中包含所有节点,排序完成。
- 如果图中存在环,比如1->2,2->3,3->1。在该图中没有入度为0的节点,无法选数。
- 所以拓扑排序有一个重要的应用,判断有向图是否带环,如果不带环,则排序结果包含所有的节点;反之,该图带环。
拓扑排序的结果有这几种可能:
- 1 2 3 4 5
- 1 4 2 3 5
- 1 4 2 5 3
- 1 2 4 5 3
- 1 2 4 3 5
可以发现,拓扑排序的结果不是唯一的。
核心思想
- 目标:将图中的节点按依赖关系线性化,确保所有前驱节点优先于后继节点。
- 适用条件:仅适用于无环的有向图(若图中有环,则无法完成拓扑排序)。
实现方法
1. Kahn算法(基于贪心策略)
该算法也就是图中的广度优先搜索(BFS)算法。
步骤:
- 初始化
- 统计图中所有节点的入度。
- 将入度为0的节点加入队列中。
- 循环处理
- 取出队列中的结果u,加入到排序结果中。
- 遍历u所指向的节点v,将v的入度减1。
- 若v的入度变为0,将v加入队列中。
- 终止条件
- 若排序结果包含所有节点 -> 成功
- 若仍有节点未处理且队列为空 -> 失败,图中有环
还有一个问题,就是如何来表示图,或者是存储图。我们可以用STL中的容器来抽象表示:
- vector<vector
> 二维数组 - unordered_map<int,vector
> 哈希表 - unordered_map<string,vector
>(如果节点是string类型的,如课程名称)
用二维数组存储图的例子
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
bool TopSort(vector<vector<int>>& graph, int n, vector<int>& inDgree)
{
int num = 0; //统计排序结果的数目
queue<int> q;
for (int i = 0; i < n; i++) //将所有入度为0的节点放入队列中
{
if (inDgree[i] == 0)
q.push(i);
}
while (q.size()) //循环处理
{
int u = q.front(); //取队首
q.pop();
cout << u << " ";
for (int v : graph[u]) //u->v
{
inDgree[v]--; //节点v入度减一
if (inDgree[v] == 0) //节点v入度为0则入队列
q.push(v);
}
num++;
}
cout << endl;
if (num == n)
return true;
else
return false;
}
int main()
{
int n, m;
cout << "请输入顶点数和边数:";
cin >> n >> m;
vector<vector<int>> G(n); //二维数组模拟邻接表存图
for (int i = 0; i < m; i++)
{
int x, y;
cout << "请输入第" << i + 1 << "条边:" ;
cin >> x >> y;
G[x].push_back(y);
}
vector<int> inDgree(n); //记录入度
for (auto x : G)
{
for (int y : x) //节点指向 x->y
{
inDgree[y]++; //y的入度++
}
}
cout << "拓扑排序结果为:";
bool ret = TopSort(G, n, inDgree);
cout << ret << endl;
return 0;
}
用哈希表存储图的例子
#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
using namespace std;
vector<string> TopSort(unordered_map<string, vector<string>>& graph)
{
unordered_map<string, int> inDgree; //记录入度
queue<string> q;
vector<string> result; //排序结果
for (auto& [u,neighbors] : graph) //初始化入度
{
if (!inDgree.count(u)) inDgree[u] = 0; //确保所有节点被记录
for (string v : neighbors)
{
inDgree[v]++;
}
}
for (auto& [node,degree] : inDgree) //入度为0的入队列
{
if (degree == 0)
{
q.push(node);
}
}
//处理队列
while (q.size())
{
string u = q.front();
q.pop();
result.push_back(u);
for (auto& v : graph[u]) //u->v
{
inDgree[v]--; //入度--
if (inDgree[v] == 0)
{
q.push(v); //入度为0,入队列
}
}
}
//检查环
if (result.size() != inDgree.size())
return {};
return result;
}
int main()
{
//课程依赖关系 (u->v)表示u是v的先修课
unordered_map<string, vector<string>> graph = {
{"C1",{"C2","C3"}},
{"C2",{"C4"}},
{"C3",{"C4"}},
{"C4",{}},
{"C5",{"C4"}}
};
//拓扑排序
vector<string> order = TopSort(graph);
if (order.empty())
cout << "图中存在环!" << endl;
else
{
cout << "拓扑排序结果为:";
for (string node : order)
{
cout << node << " ";
}
}
return 0;
}
2. 基于DFS的后序遍历法
步骤:
- 对每个未访问的节点执行DFS。
- 递归访问所有邻接节点。
- 将当前节点加入栈。
- 最终反转结果得到拓扑排序。
#include <iostream>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <string>
using namespace std;
bool dfs(const string& u,
unordered_map<string, vector<string>>& graph,
unordered_set<string>& visited,
unordered_set<string>& inStack,
vector<string>& result)
{
//该节点在访问路径中出现过,说明存在环
if (inStack.count(u)) return false;
//该节点已处理过,不再处理
if (visited.count(u)) return true;
inStack.insert(u);
visited.insert(u);
for (string v : graph[u])
{
if (!dfs(v, graph, visited, inStack, result))
return false;
}
inStack.erase(u);
result.push_back(u);
return true;
}
vector<string> TopSort(unordered_map<string, vector<string>>& graph)
{
vector<string> result; //记录结果
unordered_set<string> inStack; //记录访问路径 ,如果重复出现,说明存在环
unordered_set<string> visited; //记录访问过的节点
for (auto& [u, _] : graph)
{
if (!visited.count(u))
{
if (!dfs(u, graph, visited, inStack, result))
return {}; //存在环
}
}
return result;
}
int main()
{
//课程依赖关系 (u->v)表示u是v的先修课
unordered_map<string, vector<string>> graph = {
{"C1",{"C2","C3"}},
{"C2",{"C4"}},
{"C3",{"C4"}},
{"C4",{}},
{"C5",{"C4"}}
};
//拓扑排序
vector<string> order = TopSort(graph);
reverse(order.begin(), order.end());
if (order.empty())
cout << "图中存在环!" << endl;
else
{
cout << "拓扑排序结果为:";
for (string node : order)
{
cout << node << " ";
}
}
return 0;
}
总结
- 两种算法均能高效实现拓扑排序,时间复杂度均为O(V+E),V为顶点数,E为边数。
- 若节点类型为int,可将unordered_map替换为vector提升性能。
应用场景
1. 任务调度
在项目管理中,任务之间可能存在依赖关系,某些任务必须在其他任务完成之后才能开始。通过拓扑排序,可以确定任务的执行顺序,确保每个任务在开始之前其前置任务已经完成。
2. 课程安排
在教育领域,某些课程可能需要先修其他课程。通过拓扑排序,可以确定合理的课程修读顺序,避免循环依赖,帮助学生按照逻辑顺序学习课程内容。
3. 编译器优化
在编译过程中,源代码的不同模块之间可能存在依赖关系。拓扑排序可以帮助确定模块的编译顺序,确保每个模块在被调用之前已经被正确编译。(如Makefile)
4. 数据库查询优化
在数据库设计中,表与表之间可能存在外键约束。通过拓扑排序,可以决定表的插入或删除顺序,以避免违反外键约束。
热门推荐
这些对狗狗有益的蔬果,你家的宝贝爱吃吗?
“荔宝”带你探寻“好心精神”
如何根据负载选择合适的齿轮材料?
和同学吵架了怎么办?这份最全攻略帮你轻松化解矛盾!
与同学发生纠纷:如何化解校园矛盾,共建和谐校园
阿普唑仑是治疗焦虑症、睡眠障碍的常用药,科学服用注意4点
车贷系统审批不过怎么办?全方位解决方案指南
八字命理入门:年柱和月柱的分析方法详解
在韩国推广中国民乐:一位古筝音乐院院长的坚守与传承
生产基地大量停产三月未复产被ST,晨鸣纸业去年预亏75亿危机加剧
硬盘更换后如何保留原有数据?数据保留全面指南
浆砌块石护坡:一种常见的边坡防护工程措施
微波炉荷包蛋:简单快捷的完美早餐
歼-35正式官宣,中国避开了美踩过的坑,歼-20并非完美无缺
曲面显示器好还是直面好?曲面显示器看久了看直面变凸的怎么回事
如何预防口腔溃疡反复发作
便利市民轨网末端出行 成都公交不断扩大“公交+地铁”无缝换乘范围
“蒸汽波”和“City Pop”是什么关系?
正盐、酸式盐、碱式盐定义
中国科学家找到耐碱基因!这一重大发现将成为5亿亩盐碱地的丰收密码
金丝楠木为何如此值钱?揭秘“百木之王”的独特魅力
战神电脑打开触控板快捷键 战神笔记本触摸屏怎么打开
为何纽约被誉为NBA的第一大城市,尼克斯队却难以成为第一豪门?
草坪的草能当猫草吗
猫为何会吃草(揭秘猫咪吃草的原因及健康益处)
网友疯传的“紫色尖叫助眠神器”真的靠谱吗?这个神秘成分在发挥作用!
车管知识 | 如何办理机动车电子化转籍业务?
MBTI可以如何來推進我的職業生涯?
女 ENTP:多样职业选择及与 ENTJ 区别
早停法(Early Stopping):一种简单有效的过拟合防治方法