数据结构-------Graph-algorithms(python‘s version)
1. what is graph?
联想一下航班运行轨迹,总是从一个地点到另一个地点。如果将这些地点连接起来,就成了一张航线图,如果你想使用计算机可以理解的方式画出这张图,就很自然地需要使用到Graph这个数据结构。
1.1 图的数据结构(邻接表形式)

假设航班是从LA -----> NYC------>LONDON------>TOKYO
那这些城市就是node,从A---->B的路线关系就是graph中的edge。
所以,对于图,最直接的存储方式就是邻接表。
一般的算法默认传入的也是邻接表的形式
1.2 代码实现
"""
比如上图,0 : [ 1, 4], 1:[0, 3, 4]
literally,使用python中的字典存储表示为key---value的形式
"""
num_nodes = 5
edges = [(0, 1), (0, 4), (1, 2), (1, 3), (1, 4), (2, 3), (3, 4)]
class Graph():
def __inti__(self, nodes=None, edges=None):
if not nodes:
self.nodes = nodes
if not edges:
self.edges = edges
# data存储的是所有节点可以到达的节点,也就是邻接表
self.data = [[] for _ in range(nodes)]
for n1, n2 in edges:
# print('node1:', n1, 'node2:', n2)
self.data[n1].append(n2)
self.data[n2].append(n1)
def __repr__(self):
return '\n'.join(
['{} : {}'.format(node, nebor)
for node, nebor in enumerate(self.data)]
)
def __str__(self):
return self.__repr__()
2 对无向图的操作
2.1 增加/删除边
最简单也是最基本的操作就是向邻接表添加/删除边。
def add_edge(self, edge):
n1, n2 = edge
# self.data[i] is index
if n1 in self.data[n2] and n2 in self.data[n1]:
print('existed edge')
return
self.data[n1].append(n2)
self.data[n2].append(n1)
return n1, n2
def delete_edge(self, edge):
n1, n2 = edge
if n1 not in self.data[n2] and n2 not in self.data[n1]:
return
self.data[n1].remove(n2)
self.data[n2].remove(n1)
return n1, n2
2.2 图的广度遍历
和之前对树的遍历一致,因为要按照广度遍历,所以要依赖队列,先进先出。
我们期望直到遍历的顺序,所以定义一个bfs_order[ ]来存储图的遍历顺序。
在遍历的过程中,如果遍历过了,就不再需要再次访问,所以建立一个 visited列表来存储已经访问过的节点。
如果想知道每个节点和起始节点的距离(假设目前没有权重表示距离,node之间的距离都为1,使用dist[ ]列表存储距离,同时建立parent[ ] 来表示当前节点是从哪个节点遍历过来的。
所以bfs函数需要传入的参数就是start node。
核心思想就是:不断从队头取出当前节点,遍历其所有相邻节点,将未访问节点加入队尾
def bfs(self, root):
quene = deque([root])
bfs_order = []
visited = set([root])
# 存储距离
dist = [False] * len(self.data)
# 存储父节点
parent = [False] * len(self.data)
dist[root] = 0
while quene:
node = quene.popleft()
bfs_order.append(node)
for v_node in self.data[node]:
if v_node not in visited:
visited.add(v_node)
quene.append(v_node)
parent[v_node] = node
dist[v_node] = dist[node] + 1
return bfs_order, parent, dist
2.3 图的连通性
对于无向图,我们说图是 连通的(connected),当且仅当:
从任意一个节点出发,通过图中的边,可以访问到所有其他节点。
所以bfs可以用来检查无向图是否连通。
def is_connect(self, root):
if not root:
print('empty root')
# root 是任意节点(无向图)
quene = deque([root])
visited = set([root])
bfs_order = []
while quene:
node = quene.popleft()
bfs_order.append(node)
for v_node in self.data[node]:
if v_node not in visited:
visited.add(v_node)
quene.append(v_node)
print('bfs:', bfs_order)
return len(visited) == len(self.data)
2.4 图的深度遍历
既然树有深度遍历,那么图也有深度遍历。
但是图和树也有不同之处。对于一颗树,他的数据结构需要root,并且带有左右节点,父节点。树的深度表现为当前节点到根节点的边的多少。
不过图其实没有root,节点之间也有多条路径。图中可能存在环,甚至由许多连通分量组成。(这个在离散数学中有专门讲解,很高兴我不懂离散数学也没有自己的见解)
so 图的深度是相对某一个节点的深度,当选择不同的节点作为‘root’,每个节点的深度也不一样。但是,当我们对图执行 DFS 时,图会被动态地“拉扯”成一棵树结构。
也就是说,DFS 会隐式构造一棵从起点出发的深度优先搜索树。
因此,深度这个概念对于图来说,并不是他的固有属性,而是在DFS 过程中,从起点到某节点所经历的递归层级(调用深度)。
因为深度遍历的思想继承自Tree,所以对于图的深度遍历思想也是一样的:
从根节点出发。不断深入,直到碰到叶子节点无法继续,再回溯到最近的分叉。(但是图是有环的,所以需要一个位置来存放已经访问过的节点,来防止重复访问)
def dfs(self, root):
if root is None:
raise ValueError('empty root')
visited = set([root])
dfs_order = [root]
dfs_depth = [0] * self.nodes
parent = [-1] * self.nodes # DFS Tree 的父亲
def dfs_visit(node):
for v_node in self.data[node]:
if v_node not in visited:
visited.add(v_node)
parent[v_node] = node
dfs_order.append(v_node)
dfs_depth[v_node] = dfs_depth[node] + 1
dfs_visit(v_node)
dfs_visit(root)
return dfs_order, dfs_depth
2.5 判断图中是否有环

综上所述,
判断图中是否存在环可以总结为:
访问节点u时,u的邻接表中存在已访问过的节点,且此节点不是u的parent,则图中存在环。
def has_circle(self, root):
if root is None:
raise ValueError('empty input')
visited = set([root])
parent = [-1] * self.nodes
circle = False
def dfs(root):
# 未访问的树边
for v_node in self.data[root]:
if v_node not in visited:
visited.add(v_node)
parent[v_node] = root
dfs(v_node)
# 处理非树边
else:
if v_node != parent[root]:
circle = True
dfs(root)
return circle
3.Weighted Graphs 加权图
前面说明了,引入图的数据结构是因为现生中有非常多这样的情形,比如铁路,比如地图。
但是无向图的使用是比较有限的,更多的还是有向加权图,比如从上海到大版的航线和里程,就可以用有向加权图来表示。
此处要额外提一下,对于图来说,更重要的是边,而不是节点。如果最终想呈现的图想排除孤立节点,那节点就不是图的根本属性。
3.1 加权图的初始化
from collections import defaultdict
class w_graph():
def __init__(self, nodes=None, edges=None, directed=False, weighted=False):
self.directed = directed
self.weighted = weighted
self.data = defaultdict(list)
# key: node, value: dict of neighbor -> weight
if edges is None:
raise ValueError('empty edges')
for node in nodes:
self.add_node(node)
for edge in edges:
self.add_edge(edge)
def add_node(self, node):
if node not in self.data:
self.data[node] = []
def add_edge(self, edge):
u, v, w = edge
if self.weighted:
self.data[u].append((v, w))
else:
self.data[u].append((v))
if not self.directed:
if self.weighted:
self.data[v].append((u, w))
else:
self.data[v].append((u))
def remove_edge(self, edge):
u, v, w = edge
if u in self.data:
if self.weighted:
self.data[u] = [item for item in self.data[u] if not (item[0] == v and item[1] == w)]
else:
self.data[u] = [item for item in self.data[u] if item != v]
# 如果是无向图,还要删除 v → u
if not self.directed and v in self.data:
if self.weighted:
self.data[v] = [item for item in self.data[v] if not (item[0] == u and item[1] == w)]
else:
self.data[v] = [item for item in self.data[v] if item != u]
"""
attention
因为edges是tuple结构,不可变(immutable),更符合“边对象固定不变”的语义。
删除边最好用列表推导式匹配节点和权重,
"""
def __repr__(self):
# 输出self.data的值 u:[(v1, w1), (v2, w2)]
return '\n'.join("{}: {}".format(key, value)
for key, value in self.data.items())
def __str__(self):
return '-------Graph---------' + '\n' + self.__repr__()
# 节点列表
nodes = ["Beijing", "Shanghai", "Guangzhou", "Shenzhen", "Chengdu"]
# 边列表 (u, v, w)
edges = [
("Beijing", "Shanghai", 1200),
("Beijing", "Guangzhou", 1800),
("Shanghai", "Shenzhen", 1400),
("Guangzhou", "Shenzhen", 200),
("Shenzhen", "Chengdu", 2200),
("Chengdu", "Beijing", 1600)
]
graph1 = w_graph(nodes, edges, True, True)
print(graph1)
会得到如下格式的输出
-------Graph---------
Beijing: [('Shanghai', 1200), ('Guangzhou', 1800)]
Shanghai: [('Shenzhen', 1400)]
Guangzhou: [('Shenzhen', 200)]
Shenzhen: [('Chengdu', 2200)]
Chengdu: [('Beijing', 1600)]
3.2 最短路径 Dijkstra's algorithm
Dijkstra是一种贪心算法
PS:
(Dijkstra 假设“已经确定的最短距离不会再被更新”,这个假设在负权情况下可能被打破,所以weight不能是负数)

算法步骤
1,初始化
先将所有node标记为 unvisited。visited_node被添加到visited = set()中。
并以distance[node]表示某个节点的最短路径。初始化为distance = {node:inf}
2,对当前节点,访问他的unvisited neighbours,并计算最短路径
(取距离当前节点距离最小的节点),与初始值比较,取较小值,并更新distance。
3,将当前节点标记为以访问。
4, 结束条件
如果只是求两个特定节点的最短路径,当target出现在visited中时,就结束
如果求整张图的最短路径,当未访问节点最小 dist = ∞时,停止。
这表示未访问节点和源点不联通。
实现代码
朴素数组扫描
def shortest_path(graph, source=None, target=None):
"""
朴素 Dijkstra:数组 + 线性扫描版本。
graph.nodes : 可迭代的节点集合
graph.data : 邻接表,形如 data[u] = [(v, w), ...]
"""
# ------------------------------------------------------------------
"""
路径重构函数,
因为当前的数据都是字典存储,是无序的,
所以要想恢复路径,只能依赖parent指针
如果是列表并且node是有序数字,就可以依照索引,直接输出parent[:target]
不过这个只适合教学。现生没有这么容易
"""
def reconstruct_path(parent, source, target):
"""
根据target不断回溯parent直到source
"""
result = []
vis_parent = set()
temp = target
source = int(source) # 强制类型一致
while temp is not None:
if temp in vis_parent:
break
vis_parent.add(temp)
result.append(temp)
temp = parent.get(temp)
result.reverse()
# 如果当前target可达source,则返回路径
if result and result[0] == source:
return result
return []
def get_all_paths(parent, source, nodes):
paths = {}
for target in nodes:
paths[target] = reconstruct_path(parent, source, target)
return paths
# ===== 1. 参数检查 =====
if source is None:
raise ValueError("source node is required")
# ===== 2. 初始化容器 =====
visited = set() # 已确定最短路的节点
dist = {node: float('inf') for node in graph.nodes} # 当前最优距离
parent = {node: None for node in graph.nodes} # 路径重构指针
dist[source] = 0
while True:
# Step 1:在线性扫描中选择 dist 最小的未访问节点
curr = None
min_dist = float('inf')
for node in graph.nodes:
if node not in visited and dist[node] < min_dist:
curr = node
min_dist = dist[curr]
# Step 2:若无可选节点,算法结束(图可能非连通)
if curr is None:
break
# Step 3:若 curr 是目标节点,可提前停止
if target is not None and curr == target:
break
# Step 4:搜索邻居,更新距离
for nebor, w in graph.data.get(curr, []):
if nebor in visited:
continue
if dist[curr] + w < dist[nebor]:
dist[nebor] = dist[curr] + w
parent[nebor] = curr
# 将当前节点标记为已访问
visited.add(curr)
# print(visited)
# 如果指定了节点,返回到当前节点的路径
if target is not None:
return dist[target], reconstruct_path(parent, source, target)
"""
print('source:', source)
print('parent:', parent)
print('nodes:', graph.nodes)
"""
f_dist = '\n'.join("{}: {}".format(key, value) for key, value in dist.items())
all_path = get_all_paths(parent, source, graph.nodes)
return f_dist, all_path
num_nodes = [i for i in range(9)]
edges5 = [(0, 1, 3), (0, 3, 2), (0, 8, 4), (1, 7, 4), (2, 7, 2), (2, 3, 6),
(2, 5, 1), (3, 4, 1), (4, 8, 8), (5, 6, 8)]
graph2 = w_graph(num_nodes, edges5, weighted=True)
dist, path = shortest_path(graph2, 0)
print(dist)
print(path)
复杂度
-
朴素方法:遍历数组 → O(V)
-
主循环最多访问每个节点一次 → 外层循环 O(V)
所以扫描节点的时间复杂度为O(V^2),此外,还需要每次访问邻居的权重,这部分的复杂度为O(E)
O(n) = O(V^2 + E)
这显然是非常高的。
可以发现,复杂度主要集中在遍历数组,每次都要扫描整个 dist[] 数组找到未访问节点中距离最小的节点 → O(V)。
优先队列优化
我们需要以一个复杂度更小的数据结构来存储,未访问节点及其当前距离。
那么”能否用一个结构,随时保持当前未访问节点中距离最小的节点?“
此前学到的完全二叉树就是一个可以考虑的DS:节点在数组里可以顺序存储,左/右孩子下标可直接计算。
因为当前需要min_dist,所以选择最小堆的数据结构。
代码
def dijkstra(graph, source=None, target=None):
# ===== 1. 参数检查 =====
if source is None:
raise ValueError("source node is required")
# ===== 2. 初始化容器 =====
visited = set() # 已确定最短路的节点
dist = {node: float('inf') for node in graph.nodes} # 当前最优距离
parent = {node: None for node in graph.nodes} # 路径重构指针
heap = min_heap()
dist[source] = 0
heap.push((source, 0))
# ===== 3. 更新距离和邻居 =====
while not heap.empty():
# step 1, 找到当前的节点和对应的最短距离
curr, curr_dist = heap.pop()
if curr_dist > dist[curr]:
continue
if curr in visited:
continue
# step 2:若无可选节点,算法结束(图可能非连通)
if curr is None:
break
# Step 3:若 curr 是目标节点,可提前停止
if target is not None and curr == target:
break
# Step 4:搜索邻居,更新距离
for nebor, w in graph.data.get(curr, []):
if nebor in visited:
continue
if dist[curr] + w < dist[nebor]:
dist[nebor] = dist[curr] + w
parent[nebor] = curr
heap.push((nebor, dist[nebor]))
visited.add(curr)
return dist, parent
# 节点列表
nodes = ["Beijing", "Shanghai", "Guangzhou", "Shenzhen", "Chengdu"]
# 边列表 (u, v, w)
edges = [
("Beijing", "Shanghai", 1200),
("Beijing", "Guangzhou", 1800),
("Shanghai", "Shenzhen", 1400),
("Guangzhou", "Shenzhen", 200),
("Shenzhen", "Chengdu", 2200),
("Chengdu", "Beijing", 1600)
]
graph1 = w_graph(nodes, edges, True, True)
dist, parent = dijkstra(graph1, "Beijing")
print(dist)
print(parent)
num_nodes = [i for i in range(9)]
edges5 = [(0, 1, 3), (0, 3, 2), (0, 8, 4), (1, 7, 4), (2, 7, 2), (2, 3, 6),
(2, 5, 1), (3, 4, 1), (4, 8, 8), (5, 6, 8)]
graph2 = w_graph(num_nodes, edges5, weighted=True)
dist_1, path = dijkstra(graph2, 7)
print('---------------all shortest distance to each node-------------')
print(dist_1)
print('------------the shorted path to all nodes---------------')
print(path)
更多推荐
所有评论(0)