拓扑排序的基本概念
拓扑排序(Topological Sorting) 是针对 有向无环图(DAG) 的一种线性排序算法,它使得图中任意一对顶点u和v,若存在边u→v,则在排序中u出现在v的前面。拓扑排序在任务调度、课程安排、依赖关系分析等场景中有广泛应用。🤖🤖
两个条件:
- 每个顶点出现且只出现一次
- 若存在边A→B,则在排序中A出现在B的前面
Python实现拓扑排序的两种方法
方法1:Kahn算法(基于入度)
基本思想:
- 不断从图中选择入度为0的节点
- 将这些节点加入拓扑序列
- 从图中删除这些节点及其出边
- 重复上述过程直到图为空或不存在入度为0的节点
算法步骤:
1,计算图中所有节点的入度
2,将所有入度为0的节点放入队列
3,当队列不为空时:
- a. 取出队首节点u,加入拓扑序列
- b. 对于u的每个邻接节点v:
- 将v的入度减1
- 如果v的入度变为0,将v加入队列
4,如果拓扑序列包含所有节点,则排序成功;否则图中存在环
伪代码
function KahnAlgorithm(graph):
// 计算所有节点的入度
in_degree = [0 for each node in graph]
for each node in graph:
for each neighbor of node:
in_degree[neighbor] += 1
// 初始化队列
queue = empty queue
for each node in graph:
if in_degree[node] == 0:
queue.enqueue(node)
// 初始化结果列表
topo_order = []
count = 0
// 处理队列
while queue is not empty:
u = queue.dequeue()
topo_order.append(u)
count += 1
for each neighbor v of u:
in_degree[v] -= 1
if in_degree[v] == 0:
queue.enqueue(v)
// 检查是否有环
if count != number of nodes in graph:
return "Graph has at least one cycle"
else:
return topo_order
代码实现:
from collections import deque
def topological_sort_kahn(graph):
# 计算所有节点的入度
in_degree = {u: 0 for u in graph}
for u in graph:
for v in graph[u]:
in_degree[v] += 1
# 收集所有入度为0的节点
queue = deque([u for u in in_degree if in_degree[u] == 0])
topo_order = []
while queue:
u = queue.popleft()
topo_order.append(u)
# 移除该节点,并减少邻居节点的入度
for v in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v)
# 检查是否所有节点都被排序
if len(topo_order) == len(graph):
return topo_order
else:
return [] # 图中存在环
栗子:
考虑一个有向图:1→2→3
- 初始入度:[1:0, 2:1, 3:1]
- 将节点1加入队列
- 处理1,将2的入度减为0,加入队列
- 处理2,将3的入度减为0,加入队列
- 处理3
- 最终拓扑排序:[1,2,3]
注意事项
如果图中存在环,算法无法完成所有节点的排序
可能有多个有效的拓扑排序结果
适用于静态图,不适合动态变化的图
方法2:DFS算法
概念:
深度优先搜索(Depth-First Search, DFS)是一种用于遍历或搜索树或图的算法。它沿着树的深度遍历节点,尽可能深地搜索树的分支。
基本思想:
- 从起始节点开始访问
- 对当前访问节点的所有未访问邻接节点进行递归访问
- 当没有未访问的邻接节点时回溯
算法特点
使用栈(递归或显式栈)实现
沿着一条路径尽可能深入地搜索
可能需要标记已访问节点以避免重复访问
时间复杂度:邻接表O(V+E),邻接矩阵O(V²)
空间复杂度:O(V)
核心:
如果节点在 temp 中,说明发现了环
如果节点未被访问过:
添加到 temp 集合
递归处理所有邻居节点
从 temp 中移除该节点(回溯)
标记为永久访问
将节点加入拓扑序列
代码实现:
def topological_sort_dfs(graph):
visited = set() #永久标记已完全处理完成的节点
temp = set() # 临时标记当前DFS路径上的节点(用于检测环)
topo_order = [] #存储拓扑排序结果
def dfs(node):
if node in temp:
raise ValueError("图中存在环")
if node not in visited:
temp.add(node)
#确保处理图中的所有节点(包括不连通的组件)
for neighbor in graph.get(node, []):
dfs(neighbor)
temp.remove(node)
visited.add(node)
topo_order.append(node)
for node in graph:
if node not in visited:
dfs(node)
return topo_order[::-1]
# DFS拓扑排序是以 后序(post-order)方式将节点加入列表的,
#即一个节点在其所有依赖之后被添加。
#而拓扑排序要求每个节点在其依赖之前出现,所以需要反转结果。
使用示例
# 定义有向无环图(邻接表表示)
graph = {
'起床': ['刷牙', '穿衣'],
'刷牙': ['吃早餐'],
'穿衣': ['出门'],
'吃早餐': ['出门'],
'出门': [],
'煮咖啡': ['吃早餐'],
'看新闻': []
}
# 使用Kahn算法进行拓扑排序
print("Kahn算法结果:", topological_sort_kahn(graph))
# 使用DFS算法进行拓扑排序
print("DFS算法结果:", topological_sort_dfs(graph))
实际应用场景
- 任务调度:确定任务执行的先后顺序
- 课程安排:处理课程间的先修关系
- 软件构建:编译源代码时的依赖关系
- 工作流程:确定工作步骤的执行顺序
处理环的情况
当图中存在环时,拓扑排序无法完成。可以在算法中添加检测环的逻辑:
# 修改Kahn算法返回None当存在环时
if len(topo_order) != len(graph):
print("图中存在环,无法进行拓扑排序")
性能分析
- 时间复杂度:O(V+E),其中V是顶点数,E是边数
- 空间复杂度:O(V)
总结: 拓扑排序是解决依赖关系问题的重要算法,
Python的实现简洁高效,适合处理各种任务调度和依赖分析问题。😀😀