圖(Graph)——圖的儲存結構

dyamoo發表於2020-09-24

前言

由於圖的術語比較多,大部分的術語和實際程式設計可能沒有什麼關係,所以我這裡只記錄有必要的術語,其他的術語我簡單地列在最後。如果想要具體瞭解圖的話,可以去看看《大話資料結構》這本書籍。然後我會分兩篇部落格來記錄圖,這篇部落格主要記錄圖的兩種儲存結構,下一篇是有關圖的各種演算法。圖也是我比較少用的資料結構,以前學的時候學不懂,現在回頭再看,能夠get到裡面許多點了。

圖的定義

圖是一種比較複雜的資料結構,他的結點之間的關係可以是任意的,圖中的任意兩個資料元素之間都可能相關。下圖展示了圖的結構:
圖的結構
下面給出圖的定義:

圖(Graph)是由頂點的有窮非空集合和頂點之間邊的集合組成,通常表示為:G(V, E),其中G表示一個圖,V是圖G中頂點的集合,E是圖G中的邊的集合。

我們將圖的資料元素稱為頂點(Vertex),並且圖的頂點集合一定是有窮且非空的,即圖可以沒有邊,但一定不能沒有頂點(至少有一個)。 在圖中,任意兩個頂點之間都可能有關係,頂點之間的關係用邊來表示。

圖的分類

圖的分類有很多種,這裡主要依據邊的無向和有向,將圖分為無向圖有向圖

  • 無向邊: 若頂點vi到vj之間沒有方向,則稱這條邊為無向邊(Edge),用無序偶對(vi, vj)來表示。
  • 有向邊:若頂點vi到vj之間有方向,則稱這條邊為有向邊,也稱為弧(Arc)。用有序偶對<vi, vj>來表示有向邊,vj是弧頭, vi是弧尾,即該邊的方向是vi–> vj(注意不要把頭和尾弄反了)。

別的分類還有簡單圖、複雜圖、稀疏圖、稠密圖,具體參照書去了解。在儲存結構上稍微有點區別的就只有無向圖和有向圖了,所以這裡就拋開別的術語,單純講講這兩種圖的區別。

無向圖

頂點與頂點之間沒有指向關係,或者說一條邊描述了兩個頂點間具有雙向關係。具體的例子有朋友圈、地鐵路線圖等。下圖為無向圖:
無向圖

有向圖

頂點與頂點之間有明確的指向關係,或者說一條邊只描述了兩個頂點之間具有一個單向的關係。具體的例子有微博的關注列表、食物鏈、程式的函式依賴圖等。下圖為有向圖:
有向圖

圖的兩種儲存結構

圖相比起線性表和樹,在儲存方面上覆雜了許多。如果說還像連結串列或者樹那樣,用一個結點和指向下一個結點的指標來作為頂點的結構,這是不合理的。因為一個圖並沒有明確的頭結點或者根節點,頂點與頂點之間也沒有明確的順序關係或者層次關係,所以這樣的儲存結構是不合理的。

而且就算採用了這樣的儲存結構,由於頂點之間的關係是不明確的,一個頂點可能與多個頂點有關係,也可能沒有關係。這樣在設計下一個頂點指標的時候只能用一個可變陣列來維護(有點類似與多叉樹),但這樣的設計不僅難以維護,操作起來也不太方便。

在前人的不屑努力下,總結出了五種比較適合圖的儲存結構。但這裡只介紹兩種比較常用的,感興趣的話可以去參考書籍。下面羅列出這五種儲存結構:

  • 鄰接矩陣
  • 鄰接表
  • 十字連結串列
  • 鄰接多重表
  • 邊集陣列

鄰接矩陣

圖的鄰接矩陣(Adjacency Matrix)儲存方式是用兩個陣列來表示圖。一個一維陣列儲存圖中的頂點資訊,一個二維陣列(稱為鄰接矩陣)來儲存圖中的便或弧的資訊。 用上面的無向圖做例子,那麼他的儲存結構應該是這樣子的:

一維陣列:

0123
ABCD

二維陣列(鄰接矩陣):

0123
00111
11010
21101
31010

一維陣列是頂點到矩陣行列下標的一個對映,真正用來表示關係(邊或弧)的是這個矩陣。

可以看到,在矩陣上面,行表示頂點,列表示邊。比如A跟B、C、D有關係,所以0行上的1、2、3列被設為了1,其他行也是同樣的道理。填入0的地方來表示這兩個頂點沒有關係,填入什麼是完全是由你的程式決定的。

可以觀察得到,無向圖的鄰接矩陣是一個對稱矩陣。這也很好理解,A跟B、C、D有關係,那麼B、C、D也會跟A產生關係。不僅如此,我們還通過鄰接矩陣計算出頂點的度。

頂點v的度(Degree)是和v相關聯的邊的數目。以頂點v為弧頭的弧的數目稱為v的入度(InDegree),反之則為出度(OutDegree)。

不難看出,頂點A的度為0+1+1+1 = 3,即在無向圖中,矩陣中的一行(只有0和1)之和就是該頂點的度。

理解了無向圖的鄰接矩陣之後,有向圖的也很好理解。以上面的有向圖為例,那麼他的鄰接矩陣就應該是這樣的:

0123
00001
11010
21000
30000

可以看出,A指向D的話,0行3列就被設為了1;B和C指向A,所以1行2行的0列都被設為了1。而且不難發現,一行表示該節點的出度,一列表示該節點的入度。比如A的出度就是0行之和,即1;A的入度就是0列支和,即2。

我們還可以用鄰接矩陣來記錄邊的權,例如A地到B地的機票需要1200元,A地到C地則需要2700元等,這種與邊或弧相關的數值叫做權(Weight)。這種帶權的圖通常稱作為網(Network)。 那麼鄰接矩陣中邊就不要用1來表示了,用邊的權值來表示就好了。然後0可能也不太適合用來表示頂點與頂點沒有關係,有可能某條邊的權值是0也說不定,這樣的話將沒有關係設為-1或者INT_MIN可能會好點。所以具體還是視情況而定,這裡就不展開細談了。

鄰接表

雖然鄰接矩陣非常方便且直觀,但有一個比較大的缺點,那就是當邊數比較少的時候,就會造成大量的空間浪費。用n×n的矩陣只是為了記錄一條邊,剩下n×n-1的空間就浪費掉了。所以我們可以換一種思路去儲存邊或者弧,比如說我們可以用連結串列來儲存邊或弧,用一個數字來儲存連結串列頭,這樣不管有多少條邊或弧,也不會浪費太多空間。我們將這種陣列與連結串列相結合的儲存方法稱為鄰接表(Adjacency list)。

直接看圖可能會比較容易理解,下圖為無向圖的鄰接表:

無向圖的鄰接表
可以看出,如果要統計無向圖某個頂點的度,那麼就需要將對應的連結串列遍歷一遍就行了。對於無向圖還好,對於統計有向圖的入度和出度可能就比較麻煩了。我們可以來看看有向圖的鄰接表:
有向圖的鄰接表
可以發現,連結串列上記錄的都是以對應頂點為弧尾的弧。這樣的話統計出度還好,直接計算對應節點的連結串列長度就好了。但要統計某個節點入度,就要遍歷所有的連結串列了。考慮到這個問題,於是就有了逆鄰接表:
有向圖的逆鄰接表
逆鄰接表與鄰接表相反,他的連結串列上記錄的是是以對應頂點為弧頭的弧,這樣就方便統計某個頂點的入度了。具體使用哪種鄰接表,還是兩種一起使用,就由具體情況而定。

然後關於弧的權值問題。其實這個問題也很好解決,只要在連結串列節點上增加一個欄位,由這個欄位來記錄弧的權值就好了。

程式碼實現

下面是鄰接矩陣的實現:

class Graph {
private:
    int **mat;
    int n;
public:
    Graph(int input_n) {
        n = input_n;
        mat = new int *[n];
        for (int i = 0; i < n; ++i) {
            mat[i] = new int[n];
            memset(mat[i], 0, sizeof(int) * n);
        }
    }
    
    ~Graph() {
        for (int i = 0; i< n; ++i) {
            delete[] mat[i];
        }
        delete[] mat;
    }
    
    void insert(int x, int y) {
        mat[x][y] = 1;
        //mat[y][x] = 1;	如果是無向圖得加上這段
    }
    
    void output() {
        for(int i = 0; i < n; ++i){
            for(int j = 0; j < n; ++j){
                cout << mat[i][j] << " ";
            }
            cout << endl;
        }
    }
};

下面是鄰接表的實現:

class LinkedListNode {
public:
    int vertex;
    LinkedListNode *next;

public:    
    LinkedListNode(int vertex_input) {
        vertex = vertex_input;
        next = NULL;
    }
};

class LinkedList {
public:
    LinkedListNode *head;

public:    
    LinkedList() {
        head = NULL;
    }
    
    ~LinkedList() {
        while (head != NULL) {
            LinkedListNode *delete_node = head;
            head = head->next;
            delete delete_node;
        }
    }
    
    void insert(int vertex) {
        LinkedListNode *node = new LinkedListNode(vertex);
        node->next = head;
        head = node;
    }
};

class Graph {
private:
    LinkedList *edges;
    int n;
    
public:
    Graph(int input_n) {
        n = input_n;
        edges = new LinkedList[n];
    }
    
    ~Graph() {
        delete[] edges;
    }
    
    void insert(int x, int y) {
        edges[x].insert(y);
        //edges[y].insert(x);	如果是無向圖得加上這段
    }
    
    void output() {
        for(int i = 0; i < n; ++i){
            cout << i << ":";
            for(auto j = edges[i].head; j != NULL; j = j->next){
                cout << " " << j->vertex;
            }
            cout << endl;
        }
    }
};

其他術語

圖還有很多很多的術語,就不詳細介紹了,具體還是參照書籍。這裡簡單列舉部分:

  • 完全圖
  • 有向完全圖
  • 鄰接點
  • 依附
  • 連通
  • 簡單路徑
  • 連通圖
  • 強連通圖
  • 連通分量
  • 強連通分量
  • 生成樹
  • 有向樹
  • 生成森林

相關文章