圖(Graph)——圖的儲存結構
前言
由於圖的術語比較多,大部分的術語和實際程式設計可能沒有什麼關係,所以我這裡只記錄有必要的術語,其他的術語我簡單地列在最後。如果想要具體瞭解圖的話,可以去看看《大話資料結構》這本書籍。然後我會分兩篇部落格來記錄圖,這篇部落格主要記錄圖的兩種儲存結構,下一篇是有關圖的各種演算法。圖也是我比較少用的資料結構,以前學的時候學不懂,現在回頭再看,能夠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)儲存方式是用兩個陣列來表示圖。一個一維陣列儲存圖中的頂點資訊,一個二維陣列(稱為鄰接矩陣)來儲存圖中的便或弧的資訊。 用上面的無向圖做例子,那麼他的儲存結構應該是這樣子的:
一維陣列:
0 | 1 | 2 | 3 |
---|---|---|---|
A | B | C | D |
二維陣列(鄰接矩陣):
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 0 | 1 | 1 | 1 |
1 | 1 | 0 | 1 | 0 |
2 | 1 | 1 | 0 | 1 |
3 | 1 | 0 | 1 | 0 |
一維陣列是頂點到矩陣行列下標的一個對映,真正用來表示關係(邊或弧)的是這個矩陣。
可以看到,在矩陣上面,行表示頂點,列表示邊。比如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)之和就是該頂點的度。
理解了無向圖的鄰接矩陣之後,有向圖的也很好理解。以上面的有向圖為例,那麼他的鄰接矩陣就應該是這樣的:
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 0 | 0 | 0 | 1 |
1 | 1 | 0 | 1 | 0 |
2 | 1 | 0 | 0 | 0 |
3 | 0 | 0 | 0 | 0 |
可以看出,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;
}
}
};
其他術語
圖還有很多很多的術語,就不詳細介紹了,具體還是參照書籍。這裡簡單列舉部分:
- 完全圖
- 有向完全圖
- 鄰接點
- 依附
- 連通
- 環
- 簡單路徑
- 連通圖
- 強連通圖
- 連通分量
- 強連通分量
- 生成樹
- 有向樹
- 生成森林
相關文章
- 圖的儲存結構
- 【資料結構——圖和圖的儲存結構】資料結構
- php圖的儲存結構PHP
- 資料結構:圖(Graph)資料結構
- 圖(1)--圖的相關術語與圖的儲存結構
- 【PHP資料結構】圖的概念和儲存結構PHP資料結構
- js資料結構--圖(graph)JS資料結構
- JS Graph (圖-資料結構)JS資料結構
- 圖說HP-lefthand儲存結構
- MySQL 5.6 InnoDB儲存引擎體系結構圖MySql儲存引擎
- 圖的儲存
- 資料結構複雜圖形儲存 PHP 版資料結構PHP
- 圖的儲存結構——鄰接矩陣與鄰接表矩陣
- 圖解vsan儲存結構/資料恢復方法圖解資料恢復
- 一篇看懂圖資料庫janusgraph儲存結構資料庫
- 庖丁解牛:GIF圖片原理和儲存結構
- 淘寶圖片儲存系統架構架構
- 儲存圖片
- 儲存結構
- 教你如何儲存抖音店鋪的商品圖片,自動儲存主圖、詳情圖
- 資料結構學習(C++)——圖【1】(基本儲存方法) (轉)資料結構C++
- Redis儲存結構以及儲存格式Redis
- Activiti 儲存圖片
- JanusGraph -- 儲存結構
- CentOS 儲存結構CentOS
- 【雲圖】如何建立雲圖(雲端儲存)
- MySQL InnoDB的儲存結構總結MySql
- 儲存新圖譜:DNA儲存的邊界與天地
- MySQL的varchar儲存原理:InnoDB記錄儲存結構MySql
- 圖論(Graph Theory)圖論Graph Theory
- 三種儲存結構
- MySQL Innodb 儲存結構 & 儲存Null值 解析MySqlNull
- C# 截圖並儲存為圖片C#
- 儲存器的層次結構
- 串的順序儲存結構
- 儲存架構|Haystack太強了!存2600億圖片架構
- 基於 Nebula Graph 構建圖學習能力
- 分散式儲存最全詳解(圖文全面總結)分散式