图论 LeetCode 专题
图的遍历、搜索与优化算法在面试中高频出现,与树和递归同等重要。
图论题目在 LeetCode 中约占总量的 8%,但在一线大厂面试中的出现率超过 20%。掌握图论的关键不在于背诵模板,而在于理解图的表示方法和算法选型。
一、图的表示与遍历基础
图的两种存储方式
# 邻接矩阵:适合稠密图
adj_matrix = [
[0, 1, 0, 1],
[1, 0, 1, 0],
[0, 1, 0, 1],
[1, 0, 1, 0]
]
# 邻接表:适合稀疏图(大多数面试题)
from collections import defaultdict
adj_list = defaultdict(list)
edges = [[0, 1], [0, 3], [1, 2], [2, 3]]
for u, v in edges:
adj_list[u].append(v)
adj_list[v].append(u) # 无向图
面试建议:除非题目明确给出稠密图特征,一律使用邻接表。
图的遍历模板
from collections import deque
def bfs(graph, start):
"""BFS 求最短路径(无权图)"""
visited = {start}
queue = deque([(start, 0)])
while queue:
node, dist = queue.popleft()
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, dist + 1))
return visited
def dfs(graph, node, visited):
"""DFS 遍历(递归版)"""
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
二、拓扑排序
题目 1:Course Schedule(LeetCode 207)
判断课程是否能全部修完(有向图是否有环)。
Kahn 算法(BFS):
from collections import deque, defaultdict
def canFinish(numCourses, prerequisites):
# 构建图和入度数组
graph = defaultdict(list)
indegree = [0] * numCourses
for course, prereq in prerequisites:
graph[prereq].append(course)
indegree[course] += 1
# 入度为 0 的节点入队
queue = deque([i for i, d in enumerate(indegree) if d == 0])
visited = 0
while queue:
node = queue.popleft()
visited += 1
for neighbor in graph[node]:
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
queue.append(neighbor)
return visited == numCourses
复杂度:时间 (O(V + E)),空间 (O(V + E))。
题目 2:Course Schedule II(LeetCode 210)
找出一种可行的修课顺序。
在上题基础上,只需记录出队顺序即可。如果最后出队节点数不足 numCourses,说明有环。
题目 3:Alien Dictionary(LeetCode 269)
根据外星单词的字典序,推导字母顺序。
思路:
- 相邻单词逐字符比较,找出第一个不同字符对 → 有向边
- 对字母构建图,拓扑排序得到顺序
def alienOrder(words):
# 构建有向图和入度
chars = set(''.join(words))
graph = {c: [] for c in chars}
indegree = {c: 0 for c in chars}
for i in range(len(words) - 1):
w1, w2 = words[i], words[i + 1]
# 检查前缀问题:["abc", "ab"] 非法
if len(w1) > len(w2) and w1[:len(w2)] == w2:
return ""
for a, b in zip(w1, w2):
if a != b:
graph[a].append(b)
indegree[b] += 1
break
# Kahn 拓扑排序
queue = deque([c for c in chars if indegree[c] == 0])
result = []
while queue:
c = queue.popleft()
result.append(c)
for neighbor in graph[c]:
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
queue.append(neighbor)
return ''.join(result) if len(result) == len(chars) else ""
三、并查集(Union-Find)
题目 4:Number of Provinces(LeetCode 547)
给定城市连接关系,求省份数量(连通分量)。
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.count = n # 连通分量数
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # 路径压缩
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return
# 按秩合并
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
self.count -= 1
def findCircleNum(isConnected):
n = len(isConnected)
uf = UnionFind(n)
for i in range(n):
for j in range(i + 1, n):
if isConnected[i][j] == 1:
uf.union(i, j)
return uf.count
题目 5:Redundant Connection(LeetCode 684)
树中多了一条边,找出这条冗余边(最后出现的)。
思路:并查集,如果两个节点已在同一集合,当前边就是冗余边。
def findRedundantConnection(edges):
n = len(edges)
uf = UnionFind(n + 1)
for u, v in edges:
if uf.find(u) == uf.find(v):
return [u, v]
uf.union(u, v)
return []
题目 6:Accounts Merge(LeetCode 721)
根据邮箱关联,合并同一人的账户。
思路:邮箱为节点,同一账户的邮箱之间连边,然后按连通分量合并。
四、最短路径
题目 7:Network Delay Time(LeetCode 743)
从节点 K 发出的信号,多久能到所有节点?
Dijkstra 算法:单源最短路径,无负权边。
import heapq
from collections import defaultdict
def networkDelayTime(times, n, k):
# 构建图
graph = defaultdict(list)
for u, v, w in times:
graph[u].append((v, w))
# Dijkstra
dist = {i: float('inf') for i in range(1, n + 1)}
dist[k] = 0
pq = [(0, k)]
while pq:
d, node = heapq.heappop(pq)
if d > dist[node]:
continue
for neighbor, weight in graph[node]:
new_dist = d + weight
if new_dist < dist[neighbor]:
dist[neighbor] = new_dist
heapq.heappush(pq, (new_dist, neighbor))
max_dist = max(dist.values())
return max_dist if max_dist < float('inf') else -1
复杂度:时间 (O((V + E) \log V)),空间 (O(V + E))。
题目 8:Cheapest Flights Within K Stops(LeetCode 787)
最多经停 K 次的最便宜航班。
Bellman-Ford 变体:限制边数的最短路径。
def findCheapestPrice(n, flights, src, dst, k):
# Bellman-Ford: dist[i][v] = 最多 i 条边到达 v 的最小成本
dist = [float('inf')] * n
dist[src] = 0
for _ in range(k + 1):
new_dist = dist[:]
for u, v, w in flights:
if dist[u] + w < new_dist[v]:
new_dist[v] = dist[u] + w
dist = new_dist
return dist[dst] if dist[dst] < float('inf') else -1
追问:“如果 K 很大怎么办?” → 提前剪枝:如果在某轮迭代中没有 relax 操作发生,可以提前终止。
五、最小生成树(MST)
题目 9:Min Cost to Connect All Points(LeetCode 1584)
连接所有点的最小成本(曼哈顿距离)。
Kruskal 算法:
def minCostConnectPoints(points):
n = len(points)
# 构建所有边
edges = []
for i in range(n):
for j in range(i + 1, n):
dist = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1])
edges.append((dist, i, j))
edges.sort()
uf = UnionFind(n)
cost = 0
edges_used = 0
for dist, u, v in edges:
if uf.find(u) != uf.find(v):
uf.union(u, v)
cost += dist
edges_used += 1
if edges_used == n - 1:
break
return cost
Prim 算法版本(稠密图更优):
import heapq
def minCostConnectPointsPrim(points):
n = len(points)
visited = [False] * n
# (cost, node)
pq = [(0, 0)]
total_cost = 0
edges_used = 0
while edges_used < n:
cost, node = heapq.heappop(pq)
if visited[node]:
continue
visited[node] = True
total_cost += cost
edges_used += 1
for neighbor in range(n):
if not visited[neighbor]:
dist = abs(points[node][0] - points[neighbor][0]) + \
abs(points[node][1] - points[neighbor][1])
heapq.heappush(pq, (dist, neighbor))
return total_cost
六、综合应用
题目 10:Word Ladder(LeetCode 127)
字典中找出从 beginWord 到 endWord 的最短转换序列。
双向 BFS:从两端同时搜索,减少搜索空间。
from collections import deque
def ladderLength(beginWord, endWord, wordList):
wordSet = set(wordList)
if endWord not in wordSet:
return 0
# 双向 BFS
front = {beginWord}
back = {endWord}
length = 1
while front:
# 每次都从较小的一端扩展
if len(front) > len(back):
front, back = back, front
next_front = set()
for word in front:
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
next_word = word[:i] + c + word[i+1:]
if next_word in back:
return length + 1
if next_word in wordSet:
next_front.add(next_word)
wordSet.remove(next_word)
front = next_front
length += 1
return 0
题目 11:Number of Islands(LeetCode 200)
二维网格中岛屿的数量。
def numIslands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != '1':
return
grid[r][c] = '0' # 标记已访问
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
dfs(r + dr, c + dc)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs(r, c)
return count
图论题解速查表
| 题目 | 编号 | 核心算法 | 难度 |
|---|---|---|---|
| Course Schedule | 207 | 拓扑排序(Kahn) | Medium |
| Course Schedule II | 210 | 拓扑排序 + 路径记录 | Medium |
| Alien Dictionary | 269 | 拓扑排序 + 字符串处理 | Hard |
| Number of Provinces | 547 | 并查集 | Medium |
| Redundant Connection | 684 | 并查集 | Medium |
| Accounts Merge | 721 | 并查集 + 哈希映射 | Medium |
| Network Delay Time | 743 | Dijkstra | Medium |
| Cheapest Flights Within K Stops | 787 | Bellman-Ford | Medium |
| Min Cost Connect Points | 1584 | Kruskal / Prim | Medium |
| Word Ladder | 127 | 双向 BFS | Hard |
| Number of Islands | 200 | DFS / BFS | Medium |
| Clone Graph | 133 | DFS / BFS | Medium |
| Pacific Atlantic Water Flow | 417 | DFS / BFS 多源搜索 | Medium |
| Evaluate Division | 399 | 并查集 / Floyd-Warshall | Medium |
面试追问
| 追问 | 回答要点 |
|---|---|
| “Kahn 和 DFS 拓扑排序怎么选?” | Kahn 更直观,DFS 后序反转代码更短,都能检测环 |
| “并查集路径压缩和按秩合并能同时用吗?” | 可以,时间复杂度接近 O(α(n)),α 是反阿克曼函数 |
| “Dijkstra 为什么不能用负权边?” | 贪心策略基于当前最短,负权可能导致已确定的最短路径被更新 |
| “Prim vs Kruskal 怎么选?” | 稠密图 Prim(O(V²) 堆优化前),稀疏图 Kruskal(O(E log E)) |
| “双向 BFS 为什么更快?” | 减少搜索空间,从 b^d 降到 2 * b^(d/2) |
| “图论题怎么识别?” | 看到"关系"“连接"“依赖"“路径"“网络"等关键词 |
关键知识点总结
| 算法 | 适用场景 | 时间复杂度 | 面试频率 |
|---|---|---|---|
| 拓扑排序 | DAG 依赖排序、课程安排 | O(V + E) | ⭐⭐⭐⭐⭐ |
| BFS | 无权图最短路径、层级遍历 | O(V + E) | ⭐⭐⭐⭐⭐ |
| DFS | 连通分量、环检测、回溯 | O(V + E) | ⭐⭐⭐⭐⭐ |
| 并查集 | 连通性、MST、集合合并 | O(α(N)) | ⭐⭐⭐⭐⭐ |
| Dijkstra | 单源最短路径(无负权) | O((V+E) log V) | ⭐⭐⭐⭐ |
| Bellman-Ford | 带负权的最短路径 | O(VE) | ⭐⭐⭐ |
| Kruskal/Prim | 最小生成树 | O(E log E) / O(V²) | ⭐⭐⭐⭐ |
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。