圖形的表示
圖形的表示有兩種方法:相鄰矩陣 (Adjacency Matrix) 與相鄰串列 (Adjacency List)。
相鄰矩陣 Adjacency Matrix
(1) 無向圖
對一個頂點數為 n 的圖形,準備一個 nxn 矩陣 A,其中:
A[i, j] = 1, if (i, j)邊存在 A[i, j] = 0, if (i, j)邊不存在

觀察上圖可知:
- 由於 D 與自己形成迴圈,所以此對角線不全為 0。然而若此圖為 Simple Graph、頂點不能與自己形成迴圈,則對角線元素均為 0。
- 此為無向圖,相鄰矩陣為對稱矩陣 (Symmetric Matrix)。由於邊 (Vi, Vj) 存在時、A[i, j] 與 A[j, i] 的值均為 1。為了節省記憶體,可以只儲存相鄰矩陣的上半部或下半部,即上三角矩陣或下三角矩陣。
- 第一行加總與第一列加總會相等、為點 A 的分支度,第二行與第二列加總會相等、為點 B 的分支度… 以此類推。
Q1: 判斷邊 (i, j) 是否存在?
if A[i, j] = 1 then 存在 else 不存在
Time: O(1)
Q2: 求頂點 i 之 degree?
- 第 i 列或第 i 行元素加總即可
Time: O(n)
Q3: 求圖形邊數?

- 邊數 = (所有元素加總 = 所有點的 Degree)÷2
Time: O(n2)
(2) 有向圖

觀察上圖可知:
- 此圖為 Simple Graph、頂點不能與自己形成迴圈,則對角線元素均為 0。
- 有向圖的相鄰矩陣未必為對稱矩陣 (Symmetric Matrix)。由於邊 (Vi, Vj) 存在時、不代表 (Vi, Vj) 會同時存在。
- 第一列加總為點 A 的分支度,第二列加總為點 B 的分支度… 以此類推。
Q1: 判斷邊 (i, j) 是否存在?
if A[i, j] = 1 then 存在 else 不存在
Time: O(1)
Q2: 求頂點 i 之 Out-degree 或 In-degree?
- 第 i 列元素加總 = Out-degree
- 第 i 行元素加總 = In-degree
Time: O(n)
Q3: 求圖形邊數?
- 所有頂點的 Out-degree 加總
Time: O(n2)
(3) 相鄰矩陣的優缺
優點:
- 適合儲存邊數非常多的圖形。
- 適合經常要判斷邊是否存在的問題,複雜度僅 O(1)。
缺點:
- 當圖形上的頂點數很多、邊數很少時,會形成稀疏矩陣 (sparse matrix),浪費記憶體空間。
- 求圖形上的邊數、判斷是否為連通圖、判斷有無Cycle形成,都需要 O(n2) 時間。
相鄰串列 Adjacency LIst
G = (V, E),|V| = n、|E| = e。若圖形包含 n 個頂點,則使用 n 個 Linked Lis存放該圖形,每個 Linked List 都代表一個頂點、和與其相鄰的頂點。
(1) 無向圖

Q1: 判斷邊 (i, j) 是否存在?
- 檢查 Vi 的鏈結串列中是否有 j Node 存在。
Time: O(Vi串列長度) ≤ O(所有Node數) = O(e)
Q2: 求頂點 i 之 Degree?
- 求 Vi 串列長度
Time: O(Vi串列長度)
Q3: 求圖形邊數?

- Vi LinkedList Nodes 意即 Vi 的串列長度、Vi 串列的 Node 總數。
Time: O(n+e)
問:為何不是 O(n*e)?
s = 0;
for i = 1 to n do //連同失敗、跑n+1次: O(n) Time
{
while(Vi 串列尚未scan完畢) //Node總數,和邊數e成正比
{
s++; //O(e)
}
}
e = s/2; //1次
-> Time: O(n+e)
(2) 有向圖

Q1: 判斷邊 (i, j) 是否存在?
- 檢查 Vi 的鏈結串列中是否有 j Node 存在。
Time: O(Vi串列長度) ≤ O(所有Node數) = O(e)
Q2: 求頂點 i 之 Out-degree 與 In-degree?
- 求 Vi 串列長度 = Out-degree
- 檢視所有串列,找出 i Node的出現次數 = In-degree
Time: O(n+e)
Q3: 求圖形邊數?

- Vi LinkedList Nodes 意即 Vi 的串列長度、Vi 串列的 Node 總數。
Time: O(n+e)
s = 0;
for i = 1 to n do //連同失敗、跑n+1次: O(n) Time
{
while(Vi 串列尚未scan完畢) //Node總數,和邊數e成正比
{
s++; //O(e)
}
}
e = s; //1次
-> Time: O(n+e)
(3) 相鄰矩陣的優缺
優點:
- 適合儲存頂點數多、但邊數很少的圖形。
- 能較彈性地使用記憶體,但實作較為複雜。
- 求圖形上的邊數、判斷是否為連通圖、判斷有無Cycle形成,僅需 O(n+e) 時間。
- (假設圖形的頂點個數 n、邊數 e。用相鄰串列存放無向圖時, 作為 LinkedList head 的 headnode 有 n 個、Node數有 2e 個;存放有向圖時,headnode 數有 n 個、Node 數有 e 個,因此圖形的運算時間皆為 O(n+e) )
缺點:
- 經常要判斷邊是否存在的問題,複雜度為 O(e)。
- 求圖形上的邊數、判斷是否為連通圖、判斷有無Cycle形成,都需要 O(n2) 時間。
實作相鄰串列的程式碼
這邊我們利用 C++ STL 的 deque 功能來建立可以前後增加刪除的陣列。
#include <stdio.h>
#include <deque>
using namespace std;
class GraphNode{
public:
int id;
deque<int>adjacency;
int discovered;
GraphNode(int _id){
id = _id;
discovered = 0;
}
void dump(){
printf("Vertex: %d, I'm adjacent to: ", id);
int i;
for(i = 0; i<adjacency.size(); i++){
printf("%d ", adjacency[i]);
}
printf("\n");
}
};
deque<GraphNode*> NodeList;
int main(){
int node_count, edge_count;
printf("Enter no of vertices: ");
scanf(" %d", &node_count);
printf("Enter no of edges: ");
scanf(" %d", &edge_count);
int i, j;
for(i = 0; i<node_count; i++){
NodeList.push_back( new GraphNode(i) );
}
printf("Enter %d pairs of verteices: \n", edge_count);
for( j = 0; j < edge_count; j++ ){
int node_1, node_2;
scanf("%d %d", &node_1, &node_2);
NodeList[node_1]->adjacency.push_back(node_2);
NodeList[node_2]->adjacency.push_back(node_1);
}
for( i = 0; i < NodeList.size(); i++ ){
NodeList[i]->dump();
}
return 0;
}
實作範例圖:

結果:

圖形走訪(Graph Traversal)
深度優先搜尋 DFS (Depth First Search)
DFS 就像試探著走迷宮,從起點開始、任意選一點與起點相鄰的點行走,行走過的點會被標記起來;再將下一個點視為起點、繼續選擇與該點相鄰的點行走… 直到發現所有的點都已被標記過,即相鄰此點的所有點都已經走過了,則退回上一個分叉路口,繼續探訪。
直到所有的點都被標記過了,則搜索完畢。

虛擬碼 (ds版)
DFS(V:起點)
visited[V] = TRUE;
for each vertex W adjacent to V do
if (not visited[W]) then
DFS(W);
end if
end for
虛擬碼 (Algo版)
- u.color = 頂點 u 的顏色
- White: 尚未拜訪過 (undiscovered)
- Gray: 若 u 為 discovered,但 u 的相鄰點 v 為 undiscovered
- Black: 結束 u 與其所有相鄰點的拜訪
- u.d = 起點 s 到 u 點的距離 ( = 邊數 = 路徑長)
- u.π = u 的父點 (意即從哪個點連到 u)
DFS(G){
for each vertex u in G //設初始值, Time: O(V)
{
u.color = White; //所有點設為白色
u.π = Nil; //所有點的父點皆未知
}
time = 0;
for each vertex u in G //Time: O(V)
{
if (u.color==White) then
{
DFS-Visit(G,u);
}
}
}
DFS-Visit(G, u){
time = time+1;
u.d = time; //u首度被discovered之time
u.color = Gray; //被拜訪,尚未結束
for each vertex v in G.adj[u]
{
if(v.color==White) then
{
v.π = u;
DFS-Visit(G, V);
}
}
u.color = Black; //u已結束
time = time+1;
u.f = time; //u的finished time
}
=>Time = O(V+E)

程式碼
#include <stdio.h>
#include <deque>
using namespace std;
class GraphNode{
public:
int id;
deque<int>adjacency;
int discovered;
GraphNode(int _id){
id = _id;
discovered = 0;
}
void dump(){
printf("Vertex: %d, I'm adjacent to: ", id);
int i;
for(i = 0; i<adjacency.size(); i++){
printf("%d ", adjacency[i]);
}
printf("\n");
};
};
deque<GraphNode*> NodeList;
void DFS(int vertex){
GraphNode* curr = NodeList[vertex];
curr->discovered = 1;
printf("%d ", vertex);
int i, next;
for( i = 0; i < curr->adjacency.size(); i++){
next = curr->adjacency[i];
if(NodeList[next]->discovered == 0){
DFS(next);
}
}
}
int main(){
int node_count, edge_count;
printf("Enter no of vertices: ");
scanf(" %d", &node_count);
printf("Enter no of edges: ");
scanf(" %d", &edge_count);
int i, j;
for(i = 0; i<node_count; i++){
NodeList.push_back( new GraphNode(i) );
}
printf("Enter %d pairs of verteices: \n", edge_count);
for( j = 0; j < edge_count; j++ ){
int node_1, node_2;
scanf("%d %d", &node_1, &node_2);
NodeList[node_1]->adjacency.push_back(node_2);
NodeList[node_2]->adjacency.push_back(node_1);
}
for( i = 0; i < NodeList.size(); i++ ){
NodeList[i]->dump();
}
printf("DFS Traversal: ");
DFS(0);
printf("\n");
return 0;
}
實作範例圖:

結果:

廣度優先走訪 BFS (Breadth First Search)
BFS 以某個頂點作為起始點,一開始拜訪該頂點、再接著拜訪該頂點的所有相鄰頂點,接下來再拜訪下一層的頂點,直到所有的頂點皆拜訪過為止。

虛擬碼 (DS 版)
BFS(V:起點)
//Assume visited[1..n]
visited[V] = TRUE;
Create(Q);
Enqueue(Q, V);
while(not Isempty(Q)) do
u = Dequeue(Q);
for each vertex w adjacent to u do{
for each vertex W adjacent to V do
if (not visited[W]) then
DFS(W);
虛擬碼 (ALGO版)
- u.color = 表示頂點 u 的顏色
- White: 尚未拜訪 (undiscover)
- Gray: 若 u 為 discovered,但 u 的相鄰點 v 為 undiscovered
- Black: 結束 u 與其所有相鄰點的拜訪
- u.d = 起點 s 到 u 點的距離 ( = 邊數 = 路徑長)
- u.π = u 的父點 (意即從哪個點連到 u)
除了頂點的資料結構之外,還需要一個 queue 來管理灰色的頂點,代號 Q。
BFS(G, s){ //s為起點
for each vertex u in G-{s} //初始值設定, Time: O(V)
{
u.color = white; //將起點以外的所有點設為白色
u.d = ∞;
u.π = Nil; //父點皆未知
}
//拜訪起點
s.color = Gray;
s.d = 0;
s.π = Nil;
//建立空佇列 Q
CreateQ(Q);
//將起點s加入Q
Enqueue(Q, s);
//Q不為空時
while(not IsEmpty(Q)) do
{
u = Dequeue(Q); //O(1)*V次, Time: O(V)
for each v in G.adj[u] do //Time: O(E)
{
if (v.color == White) then
{
v.color = Gray;
v.d = u.d+1;
v.π = u;
Enqueue(Q, v);
}
}
u.color = Black;
}
}
=> O(V)+O(V)+O(E) = O(V+E)
程式碼
#include <stdio.h>
#include <deque>
using namespace std;
class GraphNode{
public:
int id;
deque<int>adjacency;
int discovered;
GraphNode(int _id){
id = _id;
discovered = 0;
}
void dump(){
printf("Vertex: %d, I'm adjacent to: ", id);
int i;
for(i = 0; i<adjacency.size(); i++){
printf("%d ", adjacency[i]);
}
printf("\n");
};
};
deque<GraphNode*> NodeList;
void BFS(int start){
deque<int> que;
que.push_back(start);
NodeList[start]->discovered = 1;
int curr_id, adj_id;
GraphNode* curr_node;
GraphNode* adj_node;
while(que.size() != 0){
curr_id = que[0];
que.pop_front();
printf("%d ", curr_id);
curr_node = NodeList[curr_id];
for(int i = 0; i < curr_node->adjacency.size(); i++){
adj_id = curr_node->adjacency[i];
adj_node = NodeList[adj_id];
if(adj_node->discovered == 0){
adj_node->discovered = 1;
que.push_back(adj_id);
}
}
}
}
int main(){
int node_count, edge_count;
printf("Enter no of vertices: ");
scanf(" %d", &node_count);
printf("Enter no of edges: ");
scanf(" %d", &edge_count);
int i, j;
for(i = 0; i<node_count; i++){
NodeList.push_back( new GraphNode(i) );
}
printf("Enter %d pairs of verteices: \n", edge_count);
for( j = 0; j < edge_count; j++ ){
int node_1, node_2;
scanf("%d %d", &node_1, &node_2);
NodeList[node_1]->adjacency.push_back(node_2);
NodeList[node_2]->adjacency.push_back(node_1);
}
for( i = 0; i < NodeList.size(); i++ ){
NodeList[i]->dump();
}
printf("BFS Traversal: ");
BFS(0);
printf("\n");
return 0;
}
實作範例圖:

結果:











