From 6d113fa2373a526eb5d12dbc01a1f85d7a2cc7ab Mon Sep 17 00:00:00 2001 From: 3wish Date: Thu, 21 Dec 2023 18:00:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=EF=BC=9A=E9=82=BB=E6=8E=A5=E7=9F=A9?= =?UTF-8?q?=E9=98=B5=E5=92=8C=E9=82=BB=E6=8E=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graph/graph.py | 155 ++++++++++++++++++++++++++++++++++++++++++++ graph/iter_graph.py | 72 ++++++++++++++++++++ tree/avl_tree.py | 10 ++- 3 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 graph/graph.py create mode 100644 graph/iter_graph.py diff --git a/graph/graph.py b/graph/graph.py new file mode 100644 index 0000000..8f0fc56 --- /dev/null +++ b/graph/graph.py @@ -0,0 +1,155 @@ +""" +无向图:边表示两顶点之间的双向链接关系,如 A---B,即仅通过一条边,A 可达到 B,B 也可达到 A +有向图:边具有方向性,如 A --> B 或 B <-- A,即通过一条边,只能由 A 向 B 或 B 向 A + +连通图:从某个顶点出发,可以到达其余任意顶点。 +非连通图:从某个顶点出发,至少有一个顶点无法到达。 + +邻接(adjacency):当两顶点之间存在边相连时,称这两顶点“邻接“ +路径(path):从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的“路径 +度(degree):一个顶点拥有的边数 + +有权图:每条边都具有一个值(权重),表示从一个顶点到另一个顶点之间的代价/开销等.... +""" + +""" +邻接矩阵:设图的顶点数量为 n,邻接矩阵使用一个 n*n大小的矩阵来表示图,行(列)表示一个顶点 +矩阵元素代表边,用1或0表示两个顶点之间是否存在边,如: + + 1 3 5 6 顶点 +1 0 1 1 0 +3 1 0 1 0 +5 1 1 0 1 +6 0 1 0 0 + +注意这里表示的是无向图,关于矩阵关于主对角线对称 + +将 1 和 0 替换为其他的值,则可表示有权图 +""" + +# 实现一个邻接矩阵 + +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) + + for e in edges: + self.add_edge(e[0], e[1]) + + def size(self) -> int: + 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 add_edge(self, i:int, j:int): + # 参数 i, j 对应 vertices 元素索引 + if i < 0 or j < 0 or i >= self.size() or j >= self.size(): + raise IndexError() + + # 无向图中,邻接矩阵关于主对角线对称 + self.adj_mat[i][j] = 1 + self.adj_mat[j][i] = 1 + + 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 remove_edge(self, i: int, j: int): + if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j: + raise IndexError() + self.adj_mat[i][j] = 0 + self.adj_mat[j][i] = 0 + +""" +设无向图的顶点总数为 n、边总数为 m,则: + +添加边:在顶点对应链表的末尾添加边即可,因为是无向图,所以需要同时添加两个方向的边。 +删除边:在顶点对应链表中查找并删除指定边,在无向图中,需要同时删除两个方向的边 +添加顶点:在邻接表中添加一个链表,并将新增顶点作为链表头节点 +删除顶点:需遍历整个邻接表,删除包含指定顶点的所有边 +初始化:在邻接表中创建 n 个顶点和 2m 条边 +""" + +class Vertex: + """顶点类""" + def __init__(self, val: int): + self.val = val + +def vals_to_vets(vals: list[int]) -> list["Vertex"]: + """输入值列表 vals ,返回顶点列表 vets""" + return [Vertex(val) for val in vals] + + +def vets_to_vals(vets: list["Vertex"]) -> list[int]: + """输入顶点列表 vets ,返回值列表 vals""" + return [vet.val for vet in vets] + + +class GraphAdjList: + def __init__(self, edges: list[list[Vertex]]): + self.adj_list = dict[Vertex, list[Vertex]]() + for edge in edges: + self.add_vertex(edge[0]) + self.add_vertex(edge[1]) + self.add_edge(edge[0], edge[1]) + + def size(self): + return len(self.adj_list) + + def add_edge(self, vet1: Vertex, vet2: Vertex): + if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2: + raise ValueError() + + # 无向表,两边都需添加 + self.adj_list[vet1].append(vet2) + self.adj_list[vet2].append(vet1) + + def remove_edge(self, vet1: Vertex, vet2: Vertex): + if vet1 not in self.adj_list or vet2 not in self.adj_list: + raise ValueError() + + self.adj_list[vet1].remove(vet2) + self.adj_list[vet2].remove(vet1) + + def add_vertex(self, vet: Vertex): + """添加顶点""" + if vet in self.adj_list: + return + # 在邻接表中添加一个新链表 + self.adj_list[vet] = [] + + def remove_vertex(self, vet: Vertex): + """删除顶点""" + if vet not in self.adj_list: + raise ValueError() + # 在邻接表中删除顶点 vet 对应的链表 + self.adj_list.pop(vet) + # 遍历其他顶点的链表,删除所有包含 vet 的边 + for vertex in self.adj_list: + if vet in self.adj_list[vertex]: + self.adj_list[vertex].remove(vet) + + def print(self): + print("邻接表 =") + for vertex in self.adj_list: + tmp = [v.val for v in self.adj_list[vertex]] + print(f'{vertex.val}: {tmp}') diff --git a/graph/iter_graph.py b/graph/iter_graph.py new file mode 100644 index 0000000..fb14c49 --- /dev/null +++ b/graph/iter_graph.py @@ -0,0 +1,72 @@ +""" +广度优先遍历: + +广度优先遍历是一种由近及远的遍历方式,从某个节点出发,始终优先访问距离最近的顶点, +并一层层向外扩张。 + +BFS 通常借助队列来实现,代码如下所示。队列具有“先入先出”的性质, +这与 BFS 的“由近及远”的思想异曲同工。 + +1. 将遍历起始顶点 startVet 加入队列,并开启循环 +2. 在循环的每轮迭代中,弹出队首顶点并记录访问,然后将该顶点的所有邻接顶点加入到队列尾部。 +3. 循环步骤 2. ,直到所有顶点被访问完毕后结束。 + +时间复杂度: + +所有顶点都会入队并出队一次,使用 O(V) 时间;在遍历邻接顶点的过程中,由于是无向图, +因此所有边都会被访问2次,使用2O(E)时间;总体使用O(V+E)时间。 +""" +from collections import deque + +from .graph import GraphAdjList, Vertex + + +def graph_bfs(graph: GraphAdjList, start_vet: Vertex): + res = [] + visited = set[Vertex]([start_vet]) + que = deque[Vertex]([start_vet]) + while len(que) > 0: + 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 + + +""" +深度优先遍历: 深度优先遍历是一种优先走到底、无路可走再回头的遍历方式。如,从左上角顶点出发,访问当前顶点的某个邻接顶点, +直到走到尽头时返回,再继续走到尽头并返回,以此类推,直至所有顶点遍历完成。 + +这种“走到尽头再返回”的算法范式通常基于递归来实现。 + +深度优先遍历的算法流程如图 9-12 所示。 + +直虚线代表向下递推,表示开启了一个新的递归方法来访问新顶点。 +曲虚线代表向上回溯,表示此递归方法已经返回,回溯到了开启此方法的位置。 +""" + + +def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex): + """深度优先遍历 DFS 辅助函数""" + res.append(vet) # 记录访问顶点 + visited.add(vet) # 标记该顶点已被访问 + # 遍历该顶点的所有邻接顶点 + for adjVet in graph.adj_list[vet]: + if adjVet in visited: + continue # 跳过已被访问的顶点 + # 递归访问邻接顶点 + dfs(graph, visited, res, adjVet) + + +def graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]: + """深度优先遍历 DFS""" + # 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 + # 顶点遍历序列 + res = [] + # 哈希表,用于记录已被访问过的顶点 + visited = set[Vertex]() + dfs(graph, visited, res, start_vet) + return res diff --git a/tree/avl_tree.py b/tree/avl_tree.py index 64da100..220ced3 100644 --- a/tree/avl_tree.py +++ b/tree/avl_tree.py @@ -63,7 +63,7 @@ class TreeNode: child = node.right grand_child = child.left - # 以 child 为原点,将 node 向右旋转 + # 以 child 为原点,将 node 向左旋转 child.left = node node.right = grand_child # 更新节点高度 @@ -102,16 +102,20 @@ class TreeNode: # 查找插入位置: if val < node.val: + # 递归左子树插入 + # 接收旋转之后返回的根节点,并将其设为左子节点 node.left = self.insert_helper(node.left, val) elif val > node.val: + # 递归右子树插入 + # 接收旋转之后返回的根节点,并将其设为左子节点 node.right = self.insert_helper(node.right, val) else: # 重复节点不插入 return node - # 更新节点高度 + # 插入之后,更新节点高度 self.update_height(node) - + # 返回旋转之后的节点,如果未发生旋转则还是 node,如果发生了旋转,则是 node 的其中一个子节点 return self.rotate(node) def remove(self, val: int):