图算法基础教程
前置知识
建议先了解基本的图概念(顶点、边、度等)和数据结构(队列、栈)。本文预计阅读时间 20 分钟。
一、什么是图? 🌐
图(Graph)是一种由顶点(Vertex)和边(Edge)组成的数据结构,用于表示对象之间的关系。
图的类型
无向图
边没有方向,如社交网络中的好友关系。
有向图
边有方向,如网页之间的链接关系。
加权图
边有权重,如地图中城市间的距离。
图的应用场景
二、图的表示方法 📊
在实现图算法之前,我们需要选择合适的图的表示方式。
方法一:邻接矩阵
邻接矩阵实现
class GraphMatrix:
def __init__(self, vertices):
self.V = vertices
# 初始化邻接矩阵,0 表示无边
self.graph = [[0] * vertices for _ in range(vertices)]
def add_edge(self, u, v, weight=1):
"""添加边(无向图)"""
self.graph[u][v] = weight
self.graph[v][u] = weight
def display(self):
"""显示邻接矩阵"""
for row in self.graph:
print(row)
# 使用示例
g = GraphMatrix(4)
g.add_edge(0, 1, 5)
g.add_edge(0, 2, 3)
g.add_edge(1, 3, 2)
g.display()
优点:
- ✅ 判断两个顶点是否有边:O(1)
- ✅ 实现简单直观
缺点:
- ❌ 空间复杂度 O(V²),稀疏图浪费空间
- ❌ 遍历所有邻接点需要 O(V)
方法二:邻接表
邻接表实现
from collections import defaultdict
class GraphList:
def __init__(self):
# 使用字典存储邻接表
self.graph = defaultdict(list)
def add_edge(self, u, v, weight=1):
"""添加边(无向图)"""
self.graph[u].append((v, weight))
self.graph[v].append((u, weight))
def display(self):
"""显示邻接表"""
for vertex in self.graph:
print(f"{vertex}: {self.graph[vertex]}")
# 使用示例
g = GraphList()
g.add_edge(0, 1, 5)
g.add_edge(0, 2, 3)
g.add_edge(1, 3, 2)
g.display()
优点:
- ✅ 空间复杂度 O(V + E),适合稀疏图
- ✅ 遍历邻接点高效
缺点:
- ❌ 判断两个顶点是否有边需要 O(degree)
选择建议
- 稠密图(边数接近 V²)→ 邻接矩阵
- 稀疏图(边数远小于 V²)→ 邻接表
- 实际应用中,邻接表更常用
三、图的遍历算法 🔍
图遍历是许多图算法的基础,主要有两种方法:深度优先搜索(DFS)和广度优先搜索(BFS)。
1. 深度优先搜索(DFS)
DFS 类似于树的先序遍历,尽可能深地搜索图的分支。
DFS 实现(递归)
def dfs_recursive(graph, vertex, visited=None):
"""
深度优先搜索(递归版本)
:param graph: 邻接表表示的图
:param vertex: 当前顶点
:param visited: 已访问集合
"""
if visited is None:
visited = set()
visited.add(vertex)
print(vertex, end=' ') # 处理当前节点
# 递归访问所有未访问的邻居
for neighbor, weight in graph[vertex]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited)
# 测试
graph = {
0: [(1, 1), (2, 1)],
1: [(0, 1), (3, 1)],
2: [(0, 1), (3, 1)],
3: [(1, 1), (2, 1)]
}
print("DFS 遍历结果:")
dfs_recursive(graph, 0)
# 输出: 0 1 3 2
2. 广度优先搜索(BFS)
BFS 类似于树的层序遍历,逐层扩展搜索范围。
BFS 实现(迭代)
from collections import deque
def bfs(graph, start):
"""
广度优先搜索
:param graph: 邻接表表示的图
:param start: 起始顶点
"""
visited = set()
queue = deque([start])
visited.add(start)
while queue:
vertex = queue.popleft()
print(vertex, end=' ') # 处理当前节点
# 将所有未访问的邻居加入队列
for neighbor, weight in graph[vertex]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
# 测试
print("\nBFS 遍历结果:")
bfs(graph, 0)
# 输出: 0 1 2 3
DFS vs BFS 对比
| 特性 | DFS | BFS |
|---|---|---|
| 数据结构 | 栈(递归或显式) | 队列 |
| 空间复杂度 | O(V) | O(V) |
| 适用场景 | 连通性检测、拓扑排序 | 最短路径(无权图) |
| 遍历顺序 | 深度优先 | 广度优先 |
注意事项
- 对于非连通图,需要多次调用 DFS/BFS
- BFS 可以找到无权图的最短路径
- DFS 更适合检测环和拓扑排序
四、最短路径算法 🛣️
最短路径问题是图算法中最经典的问题之一。
1. Dijkstra 算法
Dijkstra 算法用于求解加权图中单源最短路径,要求权重非负。
Dijkstra 算法实现
import heapq
def dijkstra(graph, start):
"""
Dijkstra 最短路径算法
:param graph: 邻接表 {节点: [(邻居, 权重), ...]}
:param start: 起始节点
:return: 到各节点的最短距离
"""
# 初始化距离
dist = {node: float('inf') for node in graph}
dist[start] = 0
# 优先队列:(距离, 节点)
pq = [(0, start)]
visited = set()
while pq:
current_dist, u = heapq.heappop(pq)
# 跳过已处理的节点
if u in visited:
continue
visited.add(u)
# 松弛操作
for v, weight in graph[u]:
distance = current_dist + weight
# 如果找到更短的路径
if distance < dist[v]:
dist[v] = distance
heapq.heappush(pq, (distance, v))
return dist
# 测试
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('A', 1), ('C', 2), ('D', 5)],
'C': [('A', 4), ('B', 2), ('D', 1)],
'D': [('B', 5), ('C', 1)]
}
distances = dijkstra(graph, 'A')
print("从 A 出发的最短距离:")
for node, dist in distances.items():
print(f"A -> {node}: {dist}")
时间复杂度:O((V + E) log V),使用优先队列优化
2. Bellman-Ford 算法
Bellman-Ford 算法可以处理负权重的边,并能检测负权环。
python
def bellman_ford(graph, vertices, start):
"""
Bellman-Ford 算法
:param graph: 边列表 [(u, v, weight), ...]
:param vertices: 顶点数量
:param start: 起始节点
:return: 最短距离字典
"""
# 初始化
dist = {i: float('inf') for i in range(vertices)}
dist[start] = 0
# 松弛所有边 V-1 次
for _ in range(vertices - 1):
for u, v, weight in graph:
if dist[u] != float('inf') and dist[u] + weight < dist[v]:
dist[v] = dist[u] + weight
# 检测负权环
for u, v, weight in graph:
if dist[u] != float('inf') and dist[u] + weight < dist[v]:
return None # 存在负权环
return dist
# 测试
edges = [
(0, 1, 4),
(0, 2, 5),
(1, 2, -3),
(2, 3, 4)
]
result = bellman_ford(edges, 4, 0)
if result:
print("最短距离:", result)
else:
print("图中存在负权环")
五、最小生成树(MST) 🌳
最小生成树是连接所有顶点的边的子集,且总权重最小。
Kruskal 算法
Kruskal 算法通过贪心策略构建最小生成树。
Kruskal 算法
class UnionFind:
"""并查集数据结构"""
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * 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):
root_x, root_y = self.find(x), self.find(y)
if root_x == root_y:
return False
# 按秩合并
if self.rank[root_x] < self.rank[root_y]:
self.parent[root_x] = root_y
elif self.rank[root_x] > self.rank[root_y]:
self.parent[root_y] = root_x
else:
self.parent[root_y] = root_x
self.rank[root_x] += 1
return True
def kruskal(vertices, edges):
"""
Kruskal 最小生成树算法
:param vertices: 顶点数量
:param edges: 边列表 [(u, v, weight), ...]
:return: MST 的边列表和总权重
"""
# 按权重排序
edges.sort(key=lambda x: x[2])
uf = UnionFind(vertices)
mst = []
total_weight = 0
for u, v, weight in edges:
if uf.union(u, v): # 如果不形成环
mst.append((u, v, weight))
total_weight += weight
# MST 完成
if len(mst) == vertices - 1:
break
return mst, total_weight
# 测试
vertices = 4
edges = [
(0, 1, 10),
(0, 2, 6),
(0, 3, 5),
(1, 3, 15),
(2, 3, 4)
]
mst, weight = kruskal(vertices, edges)
print("最小生成树的边:")
for u, v, w in mst:
print(f"{u} - {v}: {w}")
print(f"总权重: {weight}")
六、实际应用案例 💼
案例一:社交网络分析
Facebook 使用图算法来分析用户关系网络:
- 好友推荐:基于共同好友数量(BFS)
- 影响力分析:PageRank 算法
- 社区发现:连通分量检测
PageRank 算法简介
PageRank 是 Google 搜索引擎的核心算法,也可用于社交网络。
基本思想:
- 一个页面的重要性取决于指向它的其他页面的重要性
- 迭代计算直到收敛
- 时间复杂度:O(k * E),k 为迭代次数
案例二:GPS 导航系统
现代导航系统结合多种图算法:
- Dijkstra/A*:最短路径规划
- 实时交通数据:动态调整边权重
- 多目标优化:考虑时间、距离、费用
案例三:网络路由
互联网路由器使用图算法决定数据包的最佳路径:
- OSPF 协议:基于 Dijkstra 算法
- BGP 协议:边界网关协议
- 负载均衡:多路径路由
七、学习建议与练习 📚
- ✅ 掌握图的两种表示方法
- ✅ 熟练实现 DFS 和 BFS
- ✅ 理解 Dijkstra 算法的原理
- ✅ 完成至少 10 道图算法题目
- ✅ 在实际项目中应用图算法
推荐练习题
LeetCode 200. 岛屿数量
给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿的数量。
难度:⭐⭐ 中等
涉及算法:DFS/BFS
LeetCode 743. 网络延迟时间
有 N 个网络节点,标记为 1 到 N。给定信号从一个节点传到另一个节点所需的时间,求从节点 K 发出的信号到达所有节点的最长时间。
难度:⭐⭐ 中等
涉及算法:Dijkstra
八、总结与展望 🎯
图算法是解决现实世界复杂问题的强大工具。
图遍历
DFS 和 BFS 是所有图算法的基础。
最短路径
Dijkstra 和 Bellman-Ford 解决路径优化问题。
最小生成树
Kruskal 和 Prim 算法优化网络连接。
实际应用
从社交网络到导航系统,无处不在。