重學數據結構之圖

1、圖

1.基本概念

  圖是一種抽象的數學結構,研究抽象對象之間的一類二元關係及其拓撲性質。而在計算機的數據結構領域中,圖則是一種複雜的數據結構。node

  一個圖是一個二元組 G = (V, E),其中:網絡

  • V 是非空有窮的頂點集合,也能夠有空圖。
  • E 是頂點偶數對的集合,即兩個頂點組成一條邊。
  • V 中的頂點也被稱爲圖 G 的頂點,E 中的邊也被稱爲圖 G 的邊。

  圖分爲無向圖和有向圖。在無向圖中邊沒有方向,用圓括號表示,例如(v1, v2)。在有向圖中邊是有方向的,用尖括號表示,例如<v1, v2>。數據結構

2.一些概念和性質

1.徹底圖

  徹底圖:任意兩個頂點之間都有邊的圖。徹底圖有以下性質:app

  • 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的一條路徑。對於路徑,還有以下概念:spa

  • 路徑的長度就是該路徑上邊的數量。
  • 成環的路徑就是起點和終點相同的路徑。
  • 簡單路徑就是不含環的路徑,也就是說除了該路徑的起點和終點可能相同外,其餘頂點均不相同。

  須要注意的是,從一個頂點到另外一個頂點,可能存在路徑,也可能不存在路徑。若是存在路徑,還可能存在多條路徑。code

3.連通圖

1)連通對象

  若是在一個無向圖中存在 vi 到 vj 的路徑,則稱這兩個頂點連通。blog

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)  # 打印圖

 

2、圖的表示和實現

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]

 

3、圖的遍歷

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,以下:

  

  使用鄰接表來表示這個圖,調用以前定義的圖類 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]
相關文章
相關標籤/搜索