【數據結構基礎】圖的存儲結構

前言

數據結構,一門數據處理的藝術,精巧的結構在一個又一個算法下發揮着他們無與倫比的高效和精密之美,在爲信息技術打下堅實地基的同時,也令無數開發者和探索者爲之着迷。java

也因如此,它做爲博主大二上學期最重要的必修課出現了。因爲你們對於上學期C++系列博文的支持,我打算將這門課的筆記也寫做系列博文,既用於整理、消化,也用於同各位交流、展現數據結構的美。node

此係列文章,將會分紅兩條主線,一條「數據結構基礎」,一條「數據結構拓展」。「數據結構基礎」主要以記錄課上內容爲主,「拓展」則是以課上內容爲基礎的更加高深的數據結構或相關應用知識。算法

歡迎關注博主,一塊兒交流、學習、進步,往期的文章將會放在文末。


基於圖的基礎概念,要在計算機中存儲一個圖,須要保存這個圖的點集和邊集。數組

保存全部節點的信息是容易的,節點老是有編號或者可編號的,所以能夠考慮使用鏈表或者線性表來存儲和快速檢索節點信息。
大多數狀況下對圖不涉及頻繁的增刪節點,而是頻繁的檢索各個節點的信息,因此仍是推薦使用數組來存儲接地那信息。
數據結構

相較於節點,保存邊集總不是那麼容易,一方面邊不能脫離節點單獨存儲,一條邊鏈接兩個節點,是兩個節點間的一個關係;另外一方面,邊集的規模比較大,在沒有重邊的狀況下,徹底無向圖邊的數量爲 C n 2 = n ( n + 1 ) 2 C^2_n=\frac{n(n+1)}{2} Cn2=2n(n+1)條邊,這個規模是 n 2 n^2 n2級別,若是選擇了不恰當的存儲結構,冗餘的空間將會是巨大的浪費。學習

那麼這一節,咱們將介紹兩種存儲圖的方法:鄰接矩陣和鄰接表spa

鄰接矩陣

鄰接矩陣是使用一個 n ∗ n n*n nn的矩陣 A = ( a i j ) A=(a_{ij}) A=(aij)來表示一張圖。用矩陣中的每個元素就表示了一對點之間的邊信息。.net

無權圖的鄰接矩陣

對於無權圖,鄰接矩陣有:設計

  • a i j = 0 a_{ij}=0 aij=0,節點i和節點j之間不存在邊
  • a i j = 1 a_{ij}=1 aij=1,節點i和節點j之間有一條邊由i指向j

對於下面的有向圖:
在這裏插入圖片描述
其鄰接矩陣爲:
在這裏插入圖片描述
從該圖中咱們能夠看到,因爲不存在指向本身的邊,因此矩陣的主對角元素都是0



3d

對於以下無向圖:
在這裏插入圖片描述
其鄰接矩陣爲:
在這裏插入圖片描述
能夠發現,對於無向圖,鄰接矩陣老是對稱的。若是節點i和節點j之間存在無向邊,則 a i j = a j i = 1 a_{ij}=a_{ji}=1 aij=aji=1。這個也給了咱們一個啓示,即無向邊能夠看做兩條方向相反的兩條有向邊。



代碼實現

在程序中,徹底可使用二維數組來模擬矩陣,咱們用矩陣a[i][j]來表示 a i j a_{ij} aij

例如,統計一張圖每一個結點的入度和出度:

輸入格式:首行爲兩個整數n,m表示圖中點的數量和邊的數量,接下來m行,每行兩個整數i和j,表明一條有向邊從i指向j

輸出格式:輸出包含2*n行,前n行爲該圖的鄰接矩陣,後n行每行三個整數k,a,b,k表示結點編號,a爲該節點入讀,b爲出度

#include<stdio.h>
int a[100][100];
int main(){ 
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i = 0,x,y;i < m;i++){ 
		scanf("%d%d",&x,&y);
		a[x][y] = 1;
	}
	for(int i = 1;i <= n;i++){ 
		for(int j = 1;j <= n;j++){ 
			printf("%d ",a[i][j]);
		}
		printf("\n");
	}
	for(int i = 1,rd,cd;i <= n;i++){ 
		rd = 0;
		for(int j = 1;j <= n;j++){ 
			rd += a[i][j];
		}
		cd = 0;
		for(int j = 1;j <= n;j++){ 
			cd += a[j][i];
		}
		printf("%d %d %d\n",i,rd,cd);
	}
}

運行結果以下:
在這裏插入圖片描述

有權圖的鄰接矩陣

對於有權圖來講,僅僅記錄兩個結點之間是否存在邊已經不足以記錄足夠的信息了,還須要記錄這些邊的權重。因而不妨規定:

  • a i j = ∞ a_{ij}=\infty aij=,節點i和節點j之間沒有邊
  • a i i = 0 a_{ii}=0 aii=0
  • a i j = v a l a_{ij}=val aij=val,節點i和節點j之間存在邊,權重爲val

例如對下面的權圖:
在這裏插入圖片描述
其鄰接矩陣爲:
在這裏插入圖片描述
對於無窮大的設置,通常來講,能夠取int的上限,起到標記的做用。其本質是取一個邊權值沒法達到的一個值,一般取一個大值,這樣便於後續有關圖算法的設計。



代碼實現

例如,統計一張有向有權圖每一個結點的出度邊權和:

輸入格式:首行爲兩個整數n,m表示圖中點的數量和邊的數量,接下來m行,每行兩個整數i,j和v,表明一條有向邊從i指向j,權值爲v

輸出格式:輸出包含2*n行,前n行爲該圖的鄰接矩陣,後n行每行三個整數k,a,k表示結點編號,a爲出度邊權和

#include<stdio.h>
int a[100][100];
const int inf = 1 << 30;
int main(){ 
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= n;i++){ 
		for(int j = 0;j <= n;j++){ 
			a[i][j] = i == j ? 0 : inf;
		}
	}
	for(int i = 0,x,y,v;i < m;i++){ 
		scanf("%d%d%d",&x,&y,&v);
		a[x][y] = v;
	}
	for(int i = 1;i <= n;i++){ 
		for(int j = 1;j <= n;j++){ 
			if(a[i][j] == inf){ 
				printf("inf ");
			}else{ 
				printf("%-4d",a[i][j]);				
			}
		}
		printf("\n");
	}
	for(int i = 1,cd;i <= n;i++){ 
		cd = 0;
		for(int j = 1;j <= n;j++){ 
			cd += a[i][j] == inf ? 0 : a[i][j];
		}
		printf("%d %d\n",i,cd);
	}
}

在這裏插入圖片描述


鄰接表

不難發現使用鄰接矩陣存儲圖結構因爲須要表示每個可能存在的邊,須要開 n 2 n^2 n2個空間。當圖中點較多且較爲稀疏時,這樣的存儲方式將會是極大地浪費。

此時可使用鄰接表來存儲圖結構

鄰接表的思想簡單來講就是爲每個節點提供一個長度可變的線性結構,每添加一個新邊,就放置在線性表後面。而這個長度可變的線性結構一般使用鏈表來實現。

在這裏插入圖片描述
它的鄰接矩陣爲:
在這裏插入圖片描述
其實對比觀察不難發現,鄰接表就是將鄰接矩陣每一行中的「1」用鏈表串聯起來且不分順序。


代碼實現

首先,使用鄰接表存儲邊信息,其結構爲鏈表,因此每一個鏈節點除了要存儲指向節點的以外至少須要有next指針。爲何是至少呢?由於一條邊可能還有邊權等等其餘字段。因此至少是指向節點和next指針,最後一個結點指向NULL

typedef struct _Edge{ 
	int vertex;
	struct _Edge * next;
}Edge;

爲了存儲鄰接表,還須要一個鏈首數組存放每一個節點鄰接鏈表的鏈首。

Edge * head[N];

則初始化圖的時候須要給head數組置空:

void init(int n){ 
	for(int i = 1;i <= n;i++){ 
		head[i] = NULL;
	}
}

當追加一條邊時,在起點的邊鏈表中插入一個節點。

void link(int x,int y){ 
	Edge * edge = (Edge*)malloc(sizeof(Edge));
	edge->vertex = y;
	edge->next = head[x];
	head[x] = edge;
}

當須要遍歷一個結點的以其爲起點的邊時,使用指針進行迭代:

void getEdges(int k){ 
	for(Edge* edge = head[k];edge != NULL;edge = edge->next){ 
		printf("%d ",edge->vertex);
	}
}
//node != NULL 可簡寫爲node

示例:

輸入格式:第一行爲兩個整數n,m表明結點個數和邊的數量,接下來m行,每行兩個整數,表示邊的起點和終點。
輸出格式:該圖的鄰接矩陣

#include<stdio.h>
#include<malloc.h>
const int N = 1000;
typedef struct _Edge{ 
	int vertex;
	struct _Edge * next;
}Edge;
Edge * head[N];
int matrix[N][N];

void link(int x,int y){ 
	Edge * edge = (Edge*)malloc(sizeof(Edge));
	edge->vertex = y;
	edge->next = head[x];
	head[x] = edge;
}

int main(){ 
	int n,m;
	scanf("%d%d",&n,&m);
	//初始化鄰接表 
	for(int i = 1;i <= n;i++){ 
		head[i] = NULL;
	}
	//讀入邊 
	for(int i = 0,x,y;i < m;i++){ 
		scanf("%d%d",&x,&y);
		link(x,y);
	} 
	//構建鄰接矩陣 
	for(int i = 1;i <= n;i++){ 
		for(Edge * edge = head[i];edge;edge = edge->next){ 
			matrix[i][edge->vertex] = 1;
		}
	}
	//打印鄰接矩陣 
	for(int i = 1;i <= n;i++){ 
		for(int j = 1;j <= n;j++){ 
			printf("%-2d",matrix[i][j]);
		}
		printf("\n"); 
	}
}

在這裏插入圖片描述

當圖無向時,能夠將一條無向邊轉化爲兩條方向相反的有向邊進行存儲。

...
scanf("%d%d",&x,&y);
link(x,y);
link(y,x);
...

當邊具備邊權等其餘字段時,須要在邊結構體中加入對應字段定義,並在建立邊的時候將這些屬性賦予邊。

void link(int x,int y,int len...){ 
	Edge * edge = (Edge*)malloc(sizeof(Edge));
	edge->vertex = y;
	edge->len = len;
	...
	edge->next = head[x];
	head[x] = edge;
}

數組模擬鄰接表

鄰接表很好,但有時候他也會戳到咱們的軟肋——動態內存管理

每個存儲的邊都是塊動態內存,當圖再也不使用的時候能想起來釋放他們老是一件難事。除此以外,當須要頻繁的建立新圖時,不斷地申請和釋放也會佔據很多的時間。

那麼有沒有靜態內存的替代方案呢?答案是確定的,可使用數組來模擬鏈表。

其實說來,就是預先開闢足夠大小的數組模擬邊結點將會申請到的內存,並定義一個指針指向當前能夠用的位置。那麼邊結點中的next指針含義也變成了下一個邊結點在數組中的下標

typedef struct _Edge{ 
	int vertex;
	int next;
}Edge;

Edge edges[M];
int pointer = 0;

爲了表示鏈表中的最後一個元素,咱們規定邊結點下標從1開始計數,這樣最後一個元素的next值能夠指向0表示結束。

與鄰接表相似,仍須要一個head數組記錄全部結點邊鏈表的鏈首,只不過這回他不用是指向該節點的指針,而是它在數組中的下標

int head[N];

在這裏插入圖片描述

同理,咱們須要在初始化時將全部結點邊鏈表置空,前面約定0表示結束,因此在這裏就是head數組置零,計數指針置零

void init(int n){ 
	for(int i = 1;i <= n;i++){ 
		head[i] = 0;
	}
	pointer = 0;
}

當添加一個元素時,直接將指針後移一個指向新邊結點。新節點的插入和鏈表插入操做相似,方便的是,給結構體賦值可使用大括號式:

void link(int x,int y){ 
	nodes[++pointer] = { y,head[x]};
	head[x] = pointer;
}

遍歷時,一樣採用迭代的方式:

void getEdge(int k){ 
	for(int i = head[k];i;i = edges[i].next){ 
	....
	}
}

用這種形式,再來實現一下上面的示例:

#include<stdio.h>
const int N = 1000;
const int M = 10000;
typedef struct _Edge{ 
	int vertex;
	int next;
}Edge;
Edge edges[M];
int head[N];
int pointer;
int matrix[N][N];

void link(int x,int y){ 
	edges[++pointer] = { y,head[x]};
	head[x] = pointer;
}

int main(){ 
	int n,m;
	scanf("%d%d",&n,&m);
	//初始化鄰接表 
	for(int i = 1;i <= n;i++){ 
		head[i] = 0;
	}
	//讀入邊 
	for(int i = 0,x,y;i < m;i++){ 
		scanf("%d%d",&x,&y);
		link(x,y);
	} 
	//構建鄰接矩陣 
	for(int i = 1;i <= n;i++){ 
		for(int j = head[i];j;j = edges[j].next){ 
			matrix[i][edges[j].vertex] = 1;
		}
	}
	//打印鄰接矩陣 
	for(int i = 1;i <= n;i++){ 
		for(int j = 1;j <= n;j++){ 
			printf("%-2d",matrix[i][j]);
		}
		printf("\n"); 
	}
}

在這裏插入圖片描述


往期博客


參考資料:

  • 《數據結構》(劉大有,楊博等編著)
  • 《算法導論》(托馬斯·科爾曼等編著)
  • 《圖解數據結構——使用Java》(胡昭民著)
  • OI WiKi
相關文章
相關標籤/搜索