数据结构与算法 图

简介: 数据结构与算法 图

图(graph)是一种非线性数据结构,由「顶点 vertex」和「边 edge」组成。我们可以将图G抽象地表示为一组顶点 𝑉 和一组边 𝐸 的集合。以下示例展示了一个包含 5 个顶点和 7 条边的图。

相较于线性关系(链表)和分治关系(树),网络关系(图)的自由度更高

根据边是否有方向:有向图和无向图

根据所有顶点是否连通:连通图和非连通图

根据边是否有权重:有权图和无权图

  • 「邻接 adjacency」:当两顶点之间存在边相连时,称这两顶点“邻接”。在图 9‑4 中,顶点 1 的邻接顶点为顶点 2、3、5。
  • 「路径 path」:从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的“路径”。在图 9‑4 中,边序列 1‑5‑2‑4 是顶点 1 到顶点 4 的一条路径。
  • 「度 degree」:一个顶点拥有的边数。对于有向图,「入度 In‑Degree」表示有多少条边指向该顶点,「出度 Out‑Degree」表示有多少条边从该顶点指出。

图的表现形式

邻接矩阵
  • 顶点不能与自身相连,因此邻接矩阵主对角线元素没有意义。
  • 对于无向图,两个方向的边等价,此时邻接矩阵关于主对角线对称。
  • 将邻接矩阵的元素从1 和 0 替换为权重,则可表示有权图。

使用邻接矩阵表示图时,我们可以直接访问矩阵元素以获取边,因此增删查操作的效率很高,时间复杂度均为 𝑂(1) 。然而,矩阵的空间复杂度为 𝑂(𝑛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 元素代表顶点索引,即对应 vertices 元素索引
        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 remove_vertex(self, index: int):
        """ 删除顶点"""
        if index >= self.size():
            raise IndexError()
        # 在顶点列表中移除索引 index 的顶点
        self.vertices.pop(index)
        # 在邻接矩阵中删除索引 index 的行
        self.adj_mat.pop(index)
        # 在邻接矩阵中删除索引 index 的列
        for row in self.adj_mat:
            row.pop(index)
  
  
    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() or i == j:
            raise IndexError()
        # 在无向图中,邻接矩阵沿主对角线对称,即满足 (i, j) == (j, i)
        self.adj_mat[i][j] = 1
        self.adj_mat[j][i] = 1
  
  
    def remove_edge(self, i: int, j: int):
        """ 删除边"""
        # 参数 i, j 对应 vertices 元素索引
        # 索引越界与相等处理
        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
  
    def print_matrix(self, mat):
        for row in mat:
            print(row)
  
    def print(self):
        """ 打印邻接矩阵"""
        print(" 顶点列表 =", self.vertices)
        print(" 邻接矩阵 =")
        self.print_matrix(self.adj_mat)
邻接表

邻接表仅存储实际存在的边,而边的总数通常远小于 𝑛 2 ,因此它更加节省空间。然而,在邻接表中需要通过遍历链表来查找边,因此其时间效率不如邻接矩阵。

邻接表结构与哈希表中的“链式地址”非常相似,因此我们也可以采用类似方法来优化效率。比
如当链表较长时,可以将链表转化为 AVL 树或红黑树,从而将时间效率从𝑂(𝑛) 优化至 𝑂(log 𝑛) ;还可以把链表转换为哈希表,从而将时间复杂度降低至 𝑂(1) 。

class GraphAdjList:
    """ 基于邻接表实现的无向图类"""
    def __init__(self, edges):
        """ 构造方法"""
        # 邻接表,key: 顶点,value:该顶点的所有邻接顶点
        self.adj_list = dict()
        # 添加所有顶点和边
        for edge in edges:
            self.add_vertex(edge[0])
            self.add_vertex(edge[1])
            self.add_edge(edge[0], edge[1])
  
  
    def size(self) -> int:
        """ 获取顶点数量"""
        return len(self.adj_list)
  
    def add_edge(self, vet1, vet2):
        """ 添加边"""
        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:
            raise ValueError()
        # 添加边 vet1 - vet2
        self.adj_list[vet1].append(vet2)
        self.adj_list[vet2].append(vet1)
  
  
    def remove_edge(self, vet1, vet2):
        """ 删除边"""
        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:
            raise ValueError()
        # 删除边 vet1 - vet2
        self.adj_list[vet1].remove(vet2)
        self.adj_list[vet2].remove(vet1)
  
  
    def add_vertex(self, vet):
        """ 添加顶点"""
        if vet in self.adj_list:
            return
        # 在邻接表中添加一个新链表
        self.adj_list[vet] = []
  
  
    def remove_vertex(self, vet):
        """ 删除顶点"""
        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},")
效率对比

似乎邻接表(哈希表)的时间与空间效率最优。但实际上,在邻接矩阵中操作边的效率更高,只需要一次数组访问或赋值操作即可。综合来看,邻接矩阵体现了“以空间换时间”的原则,而邻接表体现了“以时间换空间”的原则。

图的遍历

广度优先遍历(BFS)
from collections import deque
  
def graph_bfs(graph: GraphAdjList):
    """ 广度优先遍历 BFS"""
    # 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
    # 顶点遍历序列
    res = []
    # 哈希表,用于记录已被访问过的顶点
    visited = set([])
    # 队列用于实现 BFS
    que = deque([])
    # 以顶点 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

时间复杂度:所有顶点都会入队并出队一次,使用𝑂(|𝑉 |) 时间;在遍历邻接顶点的过程中,由于是无向图,因此所有边都会被访问2 次,使用 𝑂(2|𝐸|) 时间;总体使用𝑂(|𝑉 | + |𝐸|) 时间。

空间复杂度:列表 res ,哈希表 visited ,队列 que 中的顶点数量最多为|𝑉 | ,使用 𝑂(|𝑉 |) 空间。

深度优先遍历(DFS)

深度优先遍历是一种优先走到底、无路可走再回头的遍历方式

def dfs(graph: GraphAdjList, visited, res, vet):
    """ 深度优先遍历 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)
    """ 深度优先遍历 DFS"""
    # 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
    # 顶点遍历序列
    res = []
    # 哈希表,用于记录已被访问过的顶点
    visited = set()
    dfs(graph, visited, res, start_vet)
    return res

时间复杂度:所有顶点都会被访问1 次,使用 𝑂(|𝑉 |) 时间;所有边都会被访问2 次,使用𝑂(2|𝐸|)时间;总体使用𝑂(|𝑉 | + |𝐸|)时间。

空间复杂度:列表 res ,哈希表 visited 顶点数量最多为|𝑉 | ,递归深度最大为 |𝑉 |,因此使用𝑂(|𝑉 |)

空间。

重点回顾

  • 图由顶点和边组成,可以被表示为一组顶点和一组边构成的集合。
  • 相较于线性关系(链表)和分治关系(树),网络关系(图)具有更高的自由度,因而更为复杂。
  • 有向图的边具有方向性,连通图中的任意顶点均可达,有权图的每条边都包含权重变量。
  • 邻接矩阵利用矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用1 或 0 表示两个顶点之间有边或无边。邻接矩阵在增删查操作上效率很高,但空间占用较多。
  • 邻接表使用多个链表来表示图,第 𝑖 条链表对应顶点 𝑖 ,其中存储了该顶点的所有邻接顶点。邻接表相对于邻接矩阵更加节省空间,但由于需要遍历链表来查找边,时间效率较低。
  • 当邻接表中的链表过长时,可以将其转换为红黑树或哈希表,从而提升查询效率。
  • 从算法思想角度分析,邻接矩阵体现“以空间换时间”,邻接表体现“以时间换空间”。
  • 图可用于建模各类现实系统,如社交网络、地铁线路等。
  • 树是图的一种特例,树的遍历也是图的遍历的一种特例。
  • 图的广度优先遍历是一种由近及远、层层扩张的搜索方式,通常借助队列实现。
  • 图的深度优先遍历是一种优先走到底、无路可走时再回溯的搜索方式,常基于递归来实现。


目录
相关文章
|
算法
研究生考试.数据结构与算法之十一 图
研究生考试.数据结构与算法之十一 图
54 0
|
6月前
|
存储 算法 Python
数据结构与算法 - 图
数据结构与算法 - 图
27 0
|
存储 算法
第七章 图【数据结构与算法】3
第七章 图【数据结构与算法】3
74 0
|
算法 vr&ar
第七章 图【数据结构与算法】2
第七章 图【数据结构与算法】2
64 0
|
7月前
|
存储 人工智能 算法
第七章 图【数据结构与算法】【精致版】
第七章 图【数据结构与算法】【精致版】
78 0
|
算法 Java
图【数据结构与算法java】
图【数据结构与算法java】
54 0
|
存储 人工智能 算法
第七章 图【数据结构与算法】1
第七章 图【数据结构与算法】1
89 0
|
存储 算法
【数据结构与算法】图的概述(内含源码)
【数据结构与算法】图的概述(内含源码)
96 0
|
算法 API
【数据结构与算法】加权无线图的设计实现
【数据结构与算法】加权无线图的设计实现
130 1
【数据结构与算法】加权无线图的设计实现
|
存储 算法
数据结构/数据结构与算法实验三 图的相关算法实现
数据结构/数据结构与算法实验三 图的相关算法实现
123 0
数据结构/数据结构与算法实验三 图的相关算法实现