数据结构与算法 图


图(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 ) ,内存占用较多。
![[Pasted image 20230920191853.png]]

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},")
效率对比

![[Pasted image 20230920204255.png]]

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

图的遍历

广度优先遍历(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

![[Pasted image 20230920234821.png]]

时间复杂度:所有顶点都会入队并出队一次,使用𝑂(|𝑉 |) 时间;在遍历邻接顶点的过程中,由于是无向图,因此所有边都会被访问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 表示两个顶点之间有边或无边。邻接矩阵在增删查操作上效率很高,但空间占用较多。
  • 邻接表使用多个链表来表示图,第 𝑖 条链表对应顶点 𝑖 ,其中存储了该顶点的所有邻接顶点。邻接表相对于邻接矩阵更加节省空间,但由于需要遍历链表来查找边,时间效率较低。
  • 当邻接表中的链表过长时,可以将其转换为红黑树或哈希表,从而提升查询效率。
  • 从算法思想角度分析,邻接矩阵体现“以空间换时间”,邻接表体现“以时间换空间”。
  • 图可用于建模各类现实系统,如社交网络、地铁线路等。
  • 树是图的一种特例,树的遍历也是图的遍历的一种特例。
  • 图的广度优先遍历是一种由近及远、层层扩张的搜索方式,通常借助队列实现。
  • 图的深度优先遍历是一种优先走到底、无路可走时再回溯的搜索方式,常基于递归来实现。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值