https://www.hello-algo.com/chapter_graph/graph/#911
图的基本概念
- 图由顶点和边组成,比起链表(线性数据结构)和树(分治结构),图更自由也更复杂
方向性
- 在无向图中,边表示两个顶点之间的双向连接关系,比如微信好友
- 在有向图中,边具有方向性,比如社交网络上的关注和被关注
连通性:连通图connected graph 和 非连通图disconnected graph
- 连通图中,从某个顶点出发,可以到达任意顶点
- 非连通图中,从某个顶点出发,至少有一个顶点无法到达
有权图:为边增加权重,就得到了有权图weighted graph。例如游戏中计算共同游戏时间的亲密度
其他常用术语:
- 「邻接 adjacency」:当两顶点之间存在边相连时,称这两顶点“邻接”。
- 「路径 path」:从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的“路径”。
- 「度 degree」:一个顶点拥有的边数。对于有向图,「入度 in-degree」表示有多少条边指向该顶点,「出度 out-degree」表示有多少条边从该顶点指出。
图的表示
1.邻接矩阵表示:以下是一个无向图的邻接矩阵表示例子
它有如下特点:
- 顶点不能和自身相连,所以对角线元素无意义
- 无向图的两个方向等价,所以矩阵关于对角线对称
- 将1替代为权重,即可表示有权图
时间复杂度O(1),空间复杂度O(n^2)
2.邻接表
「邻接表 adjacency list」使用n个链表来表示图,链表节点表示顶点。第i个链表对应顶点i,其中存储了该顶点的所有邻接顶点(与该顶点相连的顶点)
边的数量显然少于n^2,所以邻接表比起矩阵,空间占用较小;但查找效率不如矩阵。
图的常见应用
顶点 | 边 | 图计算问题 | |
---|---|---|---|
社交网络 | 用户 | 好友关系 | 潜在好友推荐 |
地铁 | 站点 | 站点间连通性 | 最短路径问题 |
太阳系 | 星体 | 万有引力作用 | 轨道计算 |
图的基础操作
图的操作分为对顶点和边的操作,邻接矩阵和邻接表的实现各有不同。
基于邻接矩阵的实现
class GraphAdjMat:
"""基于邻接矩阵实现的无向图类"""
def __init__(self, vertices: list[int], edges: list[list[int]]):
"""构造方法"""
# 顶点列表,元素代表“顶点值”,索引代表“顶点索引”
self.vertices: list[int] = []
# 邻接矩阵,行列索引对应“顶点索引”
self.adj_mat: list[list[int]] = []
# 添加顶点
for val in vertices:
self.add_vertex(val)
# 添加边, edges代表的是顶点的索引
for e in edges:
self.add_edge(e[0], e[1])
def size(self):
"""获取顶点数量"""
return len(self.vertices)
def add_vertex(self, val: int):
"""添加顶点"""
n = self.size()
# 向顶点列表添加新值
self.vertices.append(val)
new_row = [0] * n
# 邻接矩阵中添加新行
self.adj_mat.append(new_row)
# 邻接矩阵中添加新列
for row in self.adj_mat:
row.append(0)
def remove_vertex(self, index: int):
"""删除顶点"""
if index > self.size():
raise IndexError()
self.vertices.pop(index)
# 在邻接矩阵中删除索引行
self.adj_mat.pop(index)
# 在临界矩阵中删除索引列
for row in self.adj_mat:
row.pop(index)
def add_edge(self, i: int, j: int):
"""添加边"""
if i < 0 or j < 0 or j > self.size() or i > self.size():
raise IndexError()
# 参数i,j对应顶点索引
self.adj_mat[i][j] = 1
self.adj_mat[j][i] = 1
def remove_edge(self, i: int, j: int):
"""删除边"""
if i < 0 or j < 0 or j > self.size() or i > self.size():
raise IndexError()
self.adj_mat[i][j] = 0
self.adj_mat[j][i] = 0
def print(self):
"""打印邻接矩阵"""
print("顶点:", self.vertices)
print("邻接矩阵:")
for row in self.adj_mat:
print(row)
基于邻接表的实现
略。https://github.com/h-kayotin/hanayo_homework/blob/master/Hello_算法/图/02_图_邻接表.py
图的遍历
图的遍历可以看做是树的一种特殊情况
广度优先遍历
由近及远,由某个顶点出发,始终优先访问最近的顶点,然后一层层向外扩张。
通常借助队列来实现
def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:
"""广度优先遍历"""
# 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
# 顶点遍历序列
res = []
# 记录已经访问过的顶点
visited = set[Vertex]([start_vet])
# 队列用于实现dfs
que = deque[Vertex]([start_vet])
# 循环访问
while len(que):
vet = que.popleft()
res.append(vet)
# 遍历所有相邻顶点
for adj_vet in graph.adj_list[vet]:
# 跳过已访问的顶点
if adj_vet in visited:
continue
# 入队未访问过的顶点
que.append(adj_vet)
visited.add(adj_vet)
return res
广度优先的遍历序列不是唯一的
深度优先遍历
从某一点出发,优先走到底,无路可走再回头。
通常借助递归实现
def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):
"""深度优先遍历辅助函数"""
res.append(vet) # 记录访问顶点
visited.add(vet) # 标记该顶点已被访问
# 遍历该顶点所有相邻顶点
for adj_vet in graph.adj_list[vet]:
if adj_vet in visited:
continue
# 递归访问相邻节点
dfs(graph, visited, res, adj_vet)
def graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:
"""深度优先遍历"""
# 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
# 顶点遍历序列
res = []
# 哈希表,用于记录已被访问过的顶点
visited = set[Vertex]()
dfs(graph, visited, res, start_vet)
return res
深度遍历的序列也不是唯一的