圖(一):圖的概念與基本操作

引言

前文介紹的樹中,元素的關係是一對多,本章介紹一種多對多的數據結構——圖。

圖的定義

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

  • V是非空有窮的頂點集合;
  • E是定點偶對(邊)的集合,E是V×V的子集
  • V的中的頂點稱為圖G的頂點,E中的邊稱為圖G的邊

圖示:

上圖被稱為有向圖,有向圖的邊有方向,是頂點的有序對;相對地,無向圖的邊沒有方向,是頂點的無序對。

圖的基本概念和性質

**完全圖:**任意兩個頂點之間都有邊的圖稱為完全圖,其性質如下:

  • 有n個頂點的無向完全圖有n×(n-1)/2條邊
  • 有n個頂點的有向完全圖有n×(n-1)條邊

**度:**一個頂點的度就是與它鄰接的邊的數目。度還分為出度和入度,出度指的是該頂點指向多少個頂點,入度指的是有多少個頂點指向該頂點。

**路徑:**如果一個頂點能夠到達另一個頂點,那麼這兩個頂點之間的邊的組合稱為路徑。路徑的性質如下:

  • 路徑長度=邊的總數
  • 環指的是起點和終點相同的路徑
  • 如果一個環除起點和終點外其他頂點都不相同,則稱為簡單環
  • 如果一條路徑中不存在環,則稱為簡單路徑

**有根圖:**如果在有向圖G里存在一個頂點v,從頂點v到圖G中其他每個頂點都有路徑,則稱呈圖G為有根圖,稱v為圖的一個根。

連通圖:

  • 連通:如果一個頂點能夠到達另外一個頂點,則稱它們是連通的
  • 連通無向圖:無向圖中如果任意兩個結點都能連通,則稱該圖為連通無向圖
  • 強連通有向圖:無向圖中如果任意兩個結點都能相互連通,則稱該圖為強連通有向圖
  • 最小連通圖:即去掉任何一條邊,該圖都無法成為連通圖。此外,最小連通無向圖的邊為n-1(頂點有n個)
  • 最小有根圖:去掉任何一條邊都無法稱為有根圖,其邊數為n-1(頂點為n)

**帶權圖:**如果圖G的每條邊都被賦予權值,則稱G為一個帶權圖。此外,帶權的連通無向圖也被稱為網絡。

圖的基本操作

圖的操作較多,主要有以下幾種:

  • 創建一個圖
  • 判斷圖是否為空
  • 獲得這個圖的頂點個數
  • 獲得這個圖的邊數
  • 獲得這個圖的頂點集合
  • 獲得這個圖的邊集合
  • 向圖中新增一個頂點
  • 將從v1到v2的邊加入這個圖
  • 獲得v1到v2邊的信息
  • 取得從v出發的所有邊
  • 檢查v的度

Python實現

圖有以下兩種常用的表示方法:

  • 鄰接矩陣表示法,缺點是鄰接矩陣較為稀疏,存在很多無用信息,浪費空間
  • 鄰接表表示法,對鄰接矩陣表示法的空間優化

鄰接矩陣表示法實現

class Graph_mat:
    def __init__(self, mat, unconn=0):
        '''
        
        Parameters
        ----------
        mat : matrix
            圖的鄰接矩陣.
        uncon : float、string、int, etc.
            主要用於表示無關聯時的數值. The default is 0.

        Returns
        -------
        None.

        '''
        vnum = len(mat) # 頂點數
        for x in mat:
            if len(x) != vnum:
                raise ValueError('mat必須是一個標準的鄰接矩陣!')
        self.mat_ = [mat[i][:] for i in range(vnum)] # 拷貝mat
        self.unconn_ = unconn
        self.vnum_ = vnum
    
    # 獲得頂點的數目
    def get_v_count(self):
        return self.vnum_
    
    # 判斷頂點下標是否越界
    def vertex_invalid(self, v):
        return v < 0 or v >= self.vnum_
    
    # 加入頂點
    def add_vertex(self, val):
        self.mat_.append([self.unconn_ for _ in range(self.vnum_)])
        self.vnum_ += 1
        for x in self.mat_:
            x.append(self.unconn_)
        self.mat_[self.vnum_-1][self.vnum_-1] = val
            
    # 加入一條邊
    def add_edge(self, v1, v2, val):
        if self.vertex_invalid(v1) or self.vertex_invalid(v2):
            raise IndexError('頂點下標越界!')
        self.mat_[v1][v2] = val
        
    # 獲取任意邊的信息
    def get_edge(self, v1, v2):
        if self.vertex_invalid(v1) or self.vertex_invalid(v2):
            raise IndexError('頂點下標越界!')
        return self.mat_[v1][v2]
    
    # 獲取所有的出邊信息
    @staticmethod
    def out_edges(row, unconn):
        output = []
        for i in range(len(row)):
            if row[i] != unconn:
                output.append([i, row[i]])
        return output


if __name__ == '__main__':
    mat = [[1,0,0,1], [0,1,0,0], [0,0,1,0], [1,0,0,1]]
    sample = Graph_mat(mat)
    print(sample.mat_)
    print('\n')
    
    print(sample.get_v_count())
    print('\n')
    
    sample.add_vertex(3)
    print(sample.mat_)
    print('\n')
    
    print(sample.get_edge(1, 2))
    print('\n')
    
    print(sample.out_edges(sample.mat_[0], 0))

鄰接表表示法實現

class Graph_AL:
    def __init__(self, mat=[], unconn=0):
        vnum = len(mat)
        for x in mat:
            if len(x) != vnum:
                raise ValueError('mat必須是一個標準的鄰接矩陣!')
        self.mat_  = [Graph_AL.out_edges(mat[i], unconn) for i in range(vnum)]
        self.vnum_ = vnum
        self.unconn_ = unconn
    
    # 獲取所有頂點數目
    def get_v_count(self):
        return self.vnum_
    
    # 判斷頂點下標是否越界
    def v_invalid(self, v):
        return v < 0 or v >= self.vnum_
    
    # 加入頂點
    def add_vertex(self, val):
        self.mat_.append([])
        self.vnum_ += 1
    
    # 加入邊
    def add_edge(self, v1, v2, val):
        if self.vnum_ == 0:
            raise GraphError('頂點數量小於2,無法形成邊!')
        if self.v_invalid(v1) or self.v_invalid(v2):
            raise IndexError('頂點下標越界!')
        row = self.mat_
        i = 0
        while i < len(row):
            # 如果已經存在這條邊的連接,那麼只需要修改權值
            if row[i][0] == v2:
                self.mat_[v1][v2] == [v2, val]
                return
            # 如果不存在這條邊,那麼跳出循環直接insert
            if row[i][0] > v2:
                break
            i += 1
        self.mat_[v1].insert(i, [v2, val])
        
    # 獲取邊的信息
    def get_edge(self, v1, v2):
        if self.vnum_ == 0:
            raise GraphError('頂點數量小於2,無法形成邊!')
        if self.v_invalid(v1) or self.v_invalid(v2):
            raise IndexError('頂點下標越界!')
        for i, val in self.mat_[v1]:
            if i == v2:
                return val
        return self.unconn_
    
    # 獲取所有的出邊信息
    @staticmethod
    def out_edges(row, unconn):
        output = []
        for i in range(len(row)):
            if row[i] != unconn:
                output.append([i, row[i]])
        return output
    
    
if __name__ == '__main__':
    mat = [[1,0,0,1], [0,1,0,0], [0,0,1,0], [1,0,0,1]]
    sample = Graph_AL(mat)
    print(sample.mat_)
    print('\n')
    
    print(sample.get_v_count())
    print('\n')
    
    sample.add_vertex(3)
    print(sample.mat_)
    print('\n')
    
    print(sample.get_edge(0, 3))
    print('\n')

參考資料

  1. 裘宗燕.數據結構與算法——Python語言描述[M].北京:機械工業出版社,2015: 224-229。
  2. 菜鳥教程:Python staticmethod函數