重学数据结构之图

一、图

1.基本概念

  图是一种抽象的数学结构,研究抽象对象之间的一类二元关系及其拓扑性质。而在计算机的数据结构领域中,图则是一种复杂的数据结构。

  一个图是一个二元组 G = (V, E),其中:

  • V 是非空有穷的顶点集合,也可以有空图。
  • E 是顶点偶数对的集合,即两个顶点组成一条边。
  • V 中的顶点也被称为图 G 的顶点,E 中的边也被称为图 G 的边。

  图分为无向图和有向图。在无向图中边没有方向,用圆括号表示,例如(v1, v2)。在有向图中边是有方向的,用尖括号表示,例如

2.一些概念和性质

1.完全图

  完全图:任意两个顶点之间都有边的图。完全图有如下性质:

  • n 个顶点的无向完全图有 n*(n-1)/2 条边。
  • n 个顶点的有向完全图有 n*(n-1) 条边。

2.路径

  对于图 G=(V, E),如果存在顶点序列如 Vi0,Vi1,...,Vin,使得 (Vi0,Vi1)、(Vi1,Vi2)、...(Vin-1,Vin)都是图的边,则说从 Vi0 到 Vin 存在路径,并称 Vi0,Vi1,...,Vin 是从顶点 Vi0 到 Vi的一条路径。对于路径,还有如下概念:

  • 路径的长度就是该路径上边的数量。
  • 成环的路径就是起点和终点相同的路径。
  • 简单路径就是不含环的路径,也就是说除了该路径的起点和终点可能相同外,其他顶点均不相同。

  需要注意的是,从一个顶点到另一个顶点,可能存在路径,也可能不存在路径。如果存在路径,还可能存在多条路径。

3.连通图

1)连通

  如果在一个无向图中存在 vi 到 vj 的路径,则称这两个顶点连通。

2)连通无向图

  如果无向图 G 中任意两个顶点之间都连通,则称 G 为连通无向图。

3)强连通有向图

  如果对于有向图 G 中任意两个顶点 vi 和 vj ,从 vi 到 vj 连通并从 vi 到 vj 也连通,则称 G 为强连通有向图。

4)带权图

  如果图 G 中的每条边都被赋予一个权值,则称 G 为带权图,可以是有向图也可以是无向图。边的权值可用于表示实际应用中与顶点之间的关联的有关信息,带权的连通无向图也被称为网络。

3.抽象数据类型

  图作为一种复杂的数据结构,可以给图定义如下操作:

ADT Graph:

  Graph(self)  # 用于创建一个图

  is_empty(self)  # 判断图是否为空 

  get_vertex(self)  # 获取顶点数量

  get_edge(self)  # 获取边的数量

  add_edge(self, v1, v2)  # 添加一条 v1 到 v2 的边

  has_edge(self, v1, v2)  # 是否有 v1 到 v2 的边

  print(self)  # 打印图

 

二、图的表示和实现

1.邻接矩阵表示法

  图的最基本表示方法是邻接矩阵表示法。邻接矩阵是表示图中顶点间邻接关系的矩阵,对于一个有 n 个顶点的图 G=(V, E),其邻接矩阵是一个 n*n 的矩阵,图中每个顶点对应于矩阵里的一行和一列。

  最简单的邻接矩阵是以0/1为元素的,即对于图 G 的邻接矩阵,当 Aij 为0时,表示顶点 vi 和 vj 无边, 当 Aij 为1时,表示顶点 vi 和 vj 有边。邻接矩阵表示了图中的顶点数量和顶点间的关系(即边),每个顶点对应矩阵的一对行列下标,听过一对下标就可以确定图中的某条边是否存在了。

  要使用 Python 实现一个图类,可以使用列表(list)或者 numpy 模块里的矩阵(array),这里就用列表来实现,即用 list 为元素的 list,具体代码如下: 

 1 # 使用邻接矩阵
 2 class Graph:
 3     def __init__(self, v):
 4         self.V = v
 5         self.E = 0
 6         self.data = [[0 for _ in range(v)] for _ in range(v)]
 7 
 8     def is_empty(self):
 9         """
10         判断图是否为空
11         :return:
12         """
13         return self.V == 0
14 
15     def get_vertex(self):
16         """
17         获取顶点数量
18         :return:
19         """
20         return self.V
21 
22     def get_edge(self):
23         """
24         获取边的数量
25         :return:
26         """
27         return self.E
28 
29     def add_edge(self, v1, v2):
30         """
31         向图中增加一条v1到v2的边
32         :param v1: 顶点
33         :param v2: 顶点
34         :return:
35         """
36         if not self.data[v1][v2]:
37             self.data[v1][v2] = 1
38             self.data[v2][v1] = 1
39             self.E += 1
40 
41     def has_edge(self, v1, v2):
42         """
43         判断是否有v1到v2的边
44         :param v1: 顶点
45         :param v2: 顶点
46         :return:
47         """
48         return self.data[v1][v2] == 1
49 
50     def print(self):
51         """
52         将图打印出来
53         :return:
54         """
55         for i in self.data:
56             print(i)

2.邻接表表示法

  用邻接矩阵表示也有一定缺点,因为图的邻接矩阵经常是稀疏的,即图中大部分元素表示的都是无边的值,为了降低图表示的空间代价,可以使用邻接表表示法。

  所谓邻接表,就是为图中每个顶点关联一个表,其中记录的是这个顶点的所有邻接边。顶点是图中最基本的部分,通常有标识,也可以顺序编号,以便可以通过编号随机访问。

  要用邻接表来表示图,这里可以继承前面的类 Graph,需要对初始化方法、增加边等方法进行修改,具体代码如下:

 1 # 使用邻接表
 2 class GraphAL(Graph):
 3     def __init__(self, v):
 4         super(GraphAL, self).__init__(v)
 5         self.V = v
 6         self.E = 0
 7         self.data = [[] for _ in range(v)]
 8 
 9     def add_edge(self, v1, v2):
10         """
11         向图中增加一条v1到v2的边
12         :param v1: 顶点
13         :param v2: 顶点
14         :return:
15         """
16         if v2 not in self.data[v1]:
17             self.data[v1].append(v2)
18             self.data[v2].append(v1)
19             self.E += 1
20 
21     def has_edge(self, v1, v2):
22         """
23         判断是否有v1到v2的边
24         :param v1: 顶点
25         :param v2: 顶点
26         :return:
27         """
28         return v2 in self.data[v1]

 

三、图的遍历

1.深度优先搜索

  按照深度优先搜索(Depth-First Search)的方法遍历整个图,假设从顶点 v 出发进行搜索,深度优先搜索的做法是:

  • 访问顶点 v,并将其标记为已经访问过。
  • 检查顶点 v 的邻接点,并从中取出一个尚未访问的顶点,从它出发继续进行深度优先搜索(递归),若不存在这种邻接点,则回溯。
  • 反复进行上述操作直至从顶点 v 出发可以到达的顶点都已经访问过。
  • 如果图中还存在尚未访问过的顶点,则从中选出一个顶点,重复进行上述过程,直至图中的所有顶点都已访问过。

2.广度优先搜索

  按照广度优先搜索(Breadth-First Search)的方法遍历整个图,假设从顶点 v 出发进行搜索,广度优先搜索的做法是:

  • 访问顶点 v,并将其标记为已经访问过。
  • 依次访问顶点 v 的所有邻接点,再对每个邻接点的所有邻接点依次进行访问,直至所有的可到达的顶点都被访问过。
  • 如果图中还存在尚未访问过的顶点,则从中选出一个顶点,重复进行上述过程,直至图中的所有顶点都已访问过。

3.深度优先搜索的实现

  前面已经说过,深度优先搜索中包含了一个重复调用的过程,因而可以使用递归来实现深度优先搜索对图进行遍历。

  在遍历的过程中,需要对已经访问过的顶点进行标记,所以定义了一个列表 visited,访问过的顶点就置为 True。除此之外,还要对访问的顶点进行记录,可以用一个列表来保存,定义为 path。具体代码如下:

 1 def graph_dfs(graph: GraphAL, s: int):
 2     """
 3     运用深度优先搜索对图进行遍历
 4     :param graph: 图
 5     :param s: 起点
 6     :return:
 7     """
 8     global visited, path, start
 9     visited = [False] * graph.get_vertex()
10     path = [0] * graph.get_vertex()
11     start = s
12     dfs(graph, s)
13 
14 
15 def dfs(graph: GraphAL, v: int):
16     """
17     深度优先搜索
18     :param graph: 图
19     :param v: 顶点
20     :return:
21     """
22     visited[v] = True
23     for i in graph.get_adj(v):
24         if not visited[i]:
25             path[i] = v
26             dfs(graph, i)

4.广度优先搜索的实现

  在使用广度优先搜索对图进行遍历的过程中,需要使用到队列,首先将起点入队,当队列非空时,重复执行以下过程:

  • 从队列中随机取出一个顶点。
  • 对该顶点的所有邻接点进行遍历,若没有访问过,则加入到路径列表 path 中并标记为访问过。

  当队列为空时,表明搜索完成,结束遍历。具体代码如下:

 1 def graph_bfs(graph: GraphAL, s: int):
 2     """
 3     运用广度优先搜索对图进行遍历
 4     :param graph: 图
 5     :param s: 起点
 6     :return:
 7     """
 8     global visited, path, start
 9     visited = [False] * graph.get_vertex()
10     path = [0] * graph.get_vertex()
11     start = s
12     bfs(graph, s)
13 
14 
15 def bfs(graph: GraphAL, v: int):
16     """
17     广度优先搜索
18     :param graph: 图
19     :param v: 顶点
20     :return:
21     """
22     visited[v] = True
23     q = Queue()
24     q.put(v)
25     while not q.empty():
26         v = q.get()
27         for node in graph.get_adj(v):
28             if not visited[node]:
29                 path[node] = v
30                 visited[node] = True
31                 q.put(node)

5.示例

  创建一个无向图 G,如下:

  重学数据结构之图_第1张图片

  使用邻接表来表示这个图,调用之前定义的图类 GraphAL,再将每条边依次添加进去,代码如下:

 1 graph_test = GraphAL(10)
 2 graph_test.add_edge(0, 1)
 3 graph_test.add_edge(1, 2)
 4 graph_test.add_edge(1, 6)
 5 graph_test.add_edge(2, 3)
 6 graph_test.add_edge(2, 7)
 7 graph_test.add_edge(3, 4)
 8 graph_test.add_edge(3, 8)
 9 graph_test.add_edge(5, 6)
10 graph_test.add_edge(7, 8)
11 graph_test.add_edge(8, 9)
12 graph_test.print()

  前面搜索过程结束之后,路径都保存在了列表 path 之中,若我们想要获取从起点到目标顶点的路径,就需要从 path 中进行获取,获取路径的方法的代码如下:

 1 def get_path_to(v: int):
 2     """
 3     获取从顶点start到顶点v的路径
 4     :param v: 目标顶点
 5     :return:
 6     """
 7     x = path[v]
 8     result = [v, x]
 9     while x != start:
10         x = path[x]
11         result.append(x)
12     print(result[::-1])

  最后就是分别使用深度优先搜索和广度优先搜索对图进行遍历,起点为顶点1,目标顶点为顶点8,最终分别使用 get_path_to() 获取路径,代码如下:

1 visited = []
2 path = []
3 start = None
4 graph_dfs(graph_test, 0)
5 get_path_to(7)
6 # [0, 1, 2, 3, 8, 7]
7 graph_bfs(graph_test, 0)
8 get_path_to(7)
9 # [0, 1, 2, 7]

你可能感兴趣的:(重学数据结构之图)