LeetCode-Py项目解析:图的存储结构详解
前言
图(Graph)是数据结构中非常重要的一种非线性结构,它比树形结构更为复杂。在实际应用中,图被广泛用于表示各种网络结构,如社交网络、交通网络、通信网络等。本文将详细介绍图的几种常见存储结构及其实现方式,帮助读者深入理解图的底层表示方法。
一、图的存储结构概述
图的结构比线性表和树更为复杂,因为它需要表示顶点和边之间的关系。一个图可能包含任意数量的顶点,并且任何两个顶点之间都可能存在边。因此,图的存储结构设计需要重点关注如何高效地表示顶点与边之间的关联关系。
常见的图的存储结构可以分为两大类:
- 顺序存储结构:包括邻接矩阵和边集数组
- 链式存储结构:包括邻接表、链式前向星、十字链表和邻接多重表
在下文中,我们将约定用n表示顶点数目,m表示边数目,TD(vᵢ)表示顶点vᵢ的度(即与该顶点相连的边数)。
二、邻接矩阵存储法
2.1 邻接矩阵原理
邻接矩阵(Adjacency Matrix)使用一个二维数组adj_matrix来存储顶点之间的邻接关系:
-
对于无权图:
- adj_matrix[i][j] = 1 表示顶点vᵢ到vⱼ存在边
- adj_matrix[i][j] = 0 表示顶点vᵢ到vⱼ不存在边
-
对于带权图:
- adj_matrix[i][j] = w(w ≠ ∞)表示顶点vᵢ到vⱼ的边权值为w
- adj_matrix[i][j] = ∞ 表示顶点vᵢ到vⱼ不存在边
2.2 邻接矩阵特点
优点:
- 实现简单直观
- 可以快速查询任意两个顶点之间是否有边存在
- 可以直接获取边的权值
缺点:
- 初始化效率和遍历效率较低
- 空间开销大(O(n²)),空间利用率低
- 不能存储重复边
- 增删节点操作不便
- 当顶点数量很大时(如n > 10⁵),内存消耗过大
2.3 时间复杂度分析
- 初始化操作:O(n²)
- 查询、添加或删除边操作:O(1)
- 获取某个点的所有边操作:O(n)
- 图的遍历操作:O(n²)
2.4 Python实现示例
class Graph:
def __init__(self, ver_count):
self.ver_count = ver_count
self.adj_matrix = [[None for _ in range(ver_count)] for _ in range(ver_count)]
def add_edge(self, vi, vj, val):
self.adj_matrix[vi][vj] = val
def get_edge(self, vi, vj):
return self.adj_matrix[vi][vj]
def printGraph(self):
for vi in range(self.ver_count):
for vj in range(self.ver_count):
val = self.get_edge(vi, vj)
if val:
print(f"{vi} - {vj} : {val}")
# 使用示例
graph = Graph(5)
edges = [[1, 2, 5], [2, 1, 5], [1, 3, 30], [3, 1, 30], [2, 3, 14], [3, 2, 14], [2, 4, 26], [4, 2, 26]]
for vi, vj, val in edges:
graph.add_edge(vi, vj, val)
graph.printGraph()
三、边集数组存储法
3.1 边集数组原理
边集数组(Edgeset Array)使用一个数组来存储所有边的信息。数组中每个元素包含:
- 边的起点vᵢ
- 边的终点vⱼ
- 边的权值val(对于带权图)
3.2 边集数组特点
优点:
- 实现简单
- 适合对边依次进行处理的运算
缺点:
- 查询边的效率低
- 计算节点度时需要遍历整个数组
- 不适合对顶点和任意边的运算
3.3 时间复杂度分析
- 图的初始化和创建:O(m)
- 查询是否存在某条边:O(m)
- 遍历某个点的所有边:O(m)
- 遍历整张图:O(nm)
3.4 Python实现示例
class EdgeNode:
def __init__(self, vi, vj, val):
self.vi = vi
self.vj = vj
self.val = val
class Graph:
def __init__(self):
self.edges = []
def add_edge(self, vi, vj, val):
self.edges.append(EdgeNode(vi, vj, val))
def get_edge(self, vi, vj):
for edge in self.edges:
if edge.vi == vi and edge.vj == vj:
return edge.val
return None
# 使用示例
graph = Graph()
edges = [[1, 2, 5], [1, 5, 6], [2, 4, 7], [4, 3, 9], [3, 1, 2], [5, 6, 8], [6, 4, 3]]
for vi, vj, val in edges:
graph.add_edge(vi, vj, val)
四、邻接表存储法
4.1 邻接表原理
邻接表(Adjacency List)结合了顺序存储和链式存储:
- 使用数组存储顶点信息(顺序存储)
- 使用链表存储每个顶点的邻接边信息(链式存储)
每个顶点对应一个链表,链表中存储与该顶点直接相连的所有边信息。
4.2 邻接表特点
优点:
- 空间利用率高(O(n+m))
- 适合稀疏图(边数远小于完全图的图)
- 可以方便地找到某个顶点的所有邻接点
缺点:
- 查询两个顶点间是否有边需要遍历链表
- 对有向图的某些操作不太方便
4.3 时间复杂度分析
- 图的初始化和创建:O(n + m)
- 查询是否存在vᵢ到vⱼ的边:O(TD(vᵢ))
- 遍历某个点的所有边:O(TD(vᵢ))
- 遍历整张图:O(n + m)
4.4 Python实现示例
class EdgeNode:
def __init__(self, vj, val):
self.vj = vj
self.val = val
self.next = None
class VertexNode:
def __init__(self, vi):
self.vi = vi
self.head = None
class Graph:
def __init__(self, ver_count):
self.ver_count = ver_count
self.vertices = [VertexNode(vi) for vi in range(ver_count)]
def add_edge(self, vi, vj, val):
edge = EdgeNode(vj, val)
edge.next = self.vertices[vi].head
self.vertices[vi].head = edge
# 使用示例
graph = Graph(7)
edges = [[1, 2, 5], [1, 5, 6], [2, 4, 7], [4, 3, 9], [3, 1, 2], [5, 6, 8], [6, 4, 3]]
for vi, vj, val in edges:
graph.add_edge(vi, vj, val)
五、链式前向星存储法
5.1 链式前向星原理
链式前向星(Linked Forward Star)是静态邻接表的一种实现方式,它结合了边集数组和邻接表的优点:
- 使用边集数组edges存储所有边
- 使用头节点数组head建立顶点到第一条边的索引关系
5.2 链式前向星特点
优点:
- 建图和遍历效率高
- 额外空间开销小
- 适合处理大规模图
缺点:
- 实现相对复杂
- 理解难度较大
5.3 Python实现示例
class EdgeNode:
def __init__(self, vj, val):
self.vj = vj
self.val = val
self.next = None
class Graph:
def __init__(self, ver_count, edge_count):
self.ver_count = ver_count
self.edge_count = edge_count
self.head = [-1] * ver_count
self.edges = []
def add_edge(self, index, vi, vj, val):
edge = EdgeNode(vj, val)
edge.next = self.head[vi]
self.edges.append(edge)
self.head[vi] = index
# 使用示例
graph = Graph(7, 7)
edges = [[1, 2, 5], [1, 5, 6], [2, 4, 7], [4, 3, 9], [3, 1, 2], [5, 6, 8], [6, 4, 3]]
for i, (vi, vj, val) in enumerate(edges):
graph.add_edge(i, vi, vj, val)
六、哈希表实现邻接表
6.1 哈希表实现原理
利用Python的字典(哈希表)可以非常简洁地实现邻接表:
- 外层字典:键是顶点,值是该顶点的邻接边字典
- 内层字典:键是边的终点,值是边的权值
6.2 Python实现示例
class Graph:
def __init__(self):
self.vertices = {}
def add_edge(self, vi, vj, val):
if vi not in self.vertices:
self.vertices[vi] = {}
self.vertices[vi][vj] = val
# 使用示例
graph = Graph()
edges = [[1, 2, 5], [1, 5, 6], [2, 4, 7], [4, 3, 9], [3, 1, 2], [5, 6, 8], [6, 4, 3]]
for vi, vj, val in edges:
graph.add_edge(vi, vj, val)
七、图论问题应用分类
图论在实际中有广泛应用,常见问题可分为以下几类:
- 图的遍历问题:深度优先搜索、广度优先搜索
- 图的连通性问题:连通分量、强连通分量、割点与桥
- 图的生成树问题:最小生成树、次小生成树
- 图的最短路径问题:Dijkstra算法、Floyd算法
- 图的网络流问题:最大流、最小费用最大流
- 二分图问题:最大匹配、最大权匹配
八、总结
本文详细介绍了图的五种存储结构及其Python实现:
- 邻接矩阵:适合稠密图,查询效率高但空间消耗大
- 边集数组:实现简单但查询效率低
- 邻接表:空间利用率高,适合稀疏图
- 链式前向星:高效存储方式,适合大规模图
- 哈希表实现:Python中简洁高效的实现方式
在实际应用中,应根据具体问题的特点选择合适的存储结构。对于算法竞赛或大规模图处理,链式前向星是很好的选择;对于小规模图或原型开发,哈希表实现更为便捷。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考