450_拓扑排序面试题¶
一句话说明¶
拓扑排序是对有向无环图(DAG)的节点排序,使所有边从前指向后,常用于课程安排、任务依赖、编译顺序等场景,BFS(Kahn 算法)和 DFS 都能实现。
核心知识点¶
白话理解¶
拓扑排序就像排课程表:学微积分前要先学高中数学,学算法前要先学数据结构。把这些先修关系理清楚,排出一个合理的学习顺序——这就是拓扑排序。有环(A依赖B依赖A)则无法排序,说明有循环依赖。
Kahn 算法(BFS)¶
- 计算所有节点的入度(有多少前置节点)
- 把入度为 0 的节点(无前置)加入队列
- 每次弹出一个节点,把它的后继节点入度 -1
- 后继节点入度变 0,加入队列
- 最终结果顺序就是拓扑顺序;如果不是所有节点都处理了,说明有环
经典题目与解法¶
题目1:课程表(LeetCode 207,检测是否有环)¶
题意:n 门课程,给出先修关系 [a, b](学 a 前必须先学 b),判断是否能完成所有课程。
from collections import deque
def can_finish(num_courses, prerequisites):
"""
Kahn 算法:拓扑排序,如果能排出所有节点说明无环
"""
# 建图:邻接表 + 入度数组
graph = [[] for _ in range(num_courses)] # graph[i] = i的后继课程
in_degree = [0] * num_courses # 每门课的前置课数量
for course, prereq in prerequisites:
graph[prereq].append(course) # prereq → course
in_degree[course] += 1 # course 入度+1
# 把所有入度为0的课(无前置)加入队列
queue = deque()
for i in range(num_courses):
if in_degree[i] == 0:
queue.append(i)
completed = 0 # 已完成的课程数
while queue:
course = queue.popleft() # 学完这门课
completed += 1
for next_course in graph[course]: # 解锁后继课程
in_degree[next_course] -= 1 # 后继课程入度-1
if in_degree[next_course] == 0: # 前置全部完成,可以学了
queue.append(next_course)
return completed == num_courses # 所有课都能完成说明无环
# 测试
print(can_finish(2, [[1,0]])) # True(先学0再学1)
print(can_finish(2, [[1,0],[0,1]])) # False(循环依赖)
print(can_finish(4, [[1,0],[2,0],[3,1],[3,2]])) # True
时间复杂度:O(V+E) V是节点数,E是边数
空间复杂度:O(V+E)
题目2:课程表 II(LeetCode 210,返回拓扑顺序)¶
题意:返回完成所有课程的学习顺序(一种合法的拓扑排序)。
def find_order(num_courses, prerequisites):
"""
在 LeetCode 207 基础上,记录处理顺序
"""
graph = [[] for _ in range(num_courses)]
in_degree = [0] * num_courses
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
queue = deque([i for i in range(num_courses) if in_degree[i] == 0])
order = [] # 记录学习顺序
while queue:
course = queue.popleft()
order.append(course) # 记录当前课程
for next_course in graph[course]:
in_degree[next_course] -= 1
if in_degree[next_course] == 0:
queue.append(next_course)
return order if len(order) == num_courses else [] # 有环返回空
# DFS 版本拓扑排序
def find_order_dfs(num_courses, prerequisites):
"""
DFS 版本:后序 DFS,最后访问的节点最先完成(逆序就是拓扑序)
"""
graph = [[] for _ in range(num_courses)]
for course, prereq in prerequisites:
graph[prereq].append(course)
WHITE, GRAY, BLACK = 0, 1, 2 # 未访问、访问中、已访问
color = [WHITE] * num_courses
order = []
has_cycle = [False]
def dfs(node):
if color[node] == GRAY: # 访问中的节点再次被访问→有环
has_cycle[0] = True
return
if color[node] == BLACK: # 已处理,跳过
return
color[node] = GRAY # 标记为访问中
for neighbor in graph[node]:
dfs(neighbor)
if has_cycle[0]:
return
color[node] = BLACK # 标记为已处理
order.append(node) # 后序:处理完所有后继才加入
for i in range(num_courses):
if color[i] == WHITE:
dfs(i)
if has_cycle[0]:
return []
return order[::-1] # 逆序就是拓扑序
# 测试
print(find_order(4, [[1,0],[2,0],[3,1],[3,2]])) # [0,1,2,3] 或 [0,2,1,3]
时间复杂度:O(V+E)
空间复杂度:O(V+E)
题目3:外星人字典(LeetCode 269,从排序结果推字母顺序)¶
题意:给出按外星语言字典序排列的单词列表,推断字母的顺序关系,返回合法的字母顺序。
from collections import defaultdict, deque
def alien_order(words):
"""
思路:
1. 比较相邻单词,找出字母顺序关系(边)
2. 对字母关系图做拓扑排序
"""
# 初始化所有出现的字母
adj = defaultdict(set) # 邻接表
in_degree = {c: 0 for word in words for c in word} # 所有字母的入度
# 从相邻单词中提取字母顺序
for i in range(len(words) - 1):
w1, w2 = words[i], words[i + 1]
min_len = min(len(w1), len(w2))
# 前缀相同但w1更长且w1出现在w2前,非法(如 ["abc", "ab"])
if len(w1) > len(w2) and w1[:min_len] == w2[:min_len]:
return ""
for j in range(min_len):
if w1[j] != w2[j]: # 找第一个不同的字母
if w2[j] not in adj[w1[j]]: # 避免重复边
adj[w1[j]].add(w2[j])
in_degree[w2[j]] += 1
break # 只取第一个不同的位置
# Kahn 拓扑排序
queue = deque([c for c in in_degree if in_degree[c] == 0])
result = []
while queue:
c = queue.popleft()
result.append(c)
for next_c in adj[c]:
in_degree[next_c] -= 1
if in_degree[next_c] == 0:
queue.append(next_c)
if len(result) != len(in_degree): # 有环
return ""
return ''.join(result)
# 测试
print(alien_order(["wrt","wrf","er","ett","rftt"])) # "wertf"
print(alien_order(["z","x"])) # "zx"
print(alien_order(["z","x","z"])) # ""(有环)
面试技巧¶
- 先判断是否有环:题目提到"依赖/先修/前置"关系,先问"有没有可能循环依赖"
- Kahn 比 DFS 更直观:入度为 0 的先排,这个思路容易解释
- 建图是关键步骤:先在纸上画出节点和边,再写代码
- 入度记录方式:
in_degree[v] += 1每次增加,出队时减邻居 - 有无唯一拓扑序:如果队列里同时有多个入度为0的节点,说明拓扑序不唯一
速查表¶
# Kahn 拓扑排序模板
from collections import deque
def topo_sort(n, edges):
graph = [[] for _ in range(n)]
in_degree = [0] * n
for u, v in edges: # u → v
graph[u].append(v)
in_degree[v] += 1
queue = deque([i for i in range(n) if in_degree[i] == 0])
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return order if len(order) == n else [] # 空列表=有环
# 检测环的简洁方式
has_cycle = (len(topo_order) != n)
| 场景 | 解法 | 关键判断 |
|---|---|---|
| 判断有无环 | Kahn或DFS三色 | 排出节点数 != 总数 = 有环 |
| 返回拓扑序 | Kahn(BFS) | 记录出队顺序 |
| 字典序最小拓扑 | 最小堆Kahn | 用heapq代替deque |
| 推断字母顺序 | 建图+拓扑 | 相邻单词比较提取边 |