1. 定義
圖是網路結構
的抽象模型,是一組由邊
連結的節點
(或定點)。
1.1 圖的表示
一個圖 G = (V, E)由以下元素組成。下圖表示一個圖
- V: 一組定點
- E: 一組邊,連線V中的定點
1.2 圖的術語
下面我們先熟悉一下圖
的術語:
- 由一條
邊
連結在一起的頂點
,稱為相鄰節點
。上圖中A
和B
是相鄰節點,A
和D
是相鄰節點。 - 一個定點的
度
是其相鄰節點
的數量
。 上圖中A
的度
是3
。=>【有向圖才會有入度和出度】 路徑
是頂點v1,v2,v3...vk
的一個連續序列。其中vi
和vi+1
是相鄰的。上圖中包含有ABEI,ABF,ACGDH,ACDG,ACDH,ADG,ADH
,這些都是簡單路徑簡單路徑
:不包含重複的定點。例如:ADG
是一條簡單路徑。環
除去最後一個節點,也是一個簡單路徑。例如ADCA
(除去最後一個頂點只剩ADC)。
1.3 有向圖 / 無向圖
圖分為無向圖
(邊沒有方向)和有向圖
(邊有方向)。上面的圖是無向圖
。下圖是有向圖
:
不對稱矩陣是有向圖
如果圖中每兩個頂點間在雙向
都存在路徑,則該圖為強聯通
的。例如下圖:A
和C
不是強聯通,而C
和D
是強聯通的。
圖還可以使加權
或未加權
的。加權圖
的邊被賦予了權值。下圖是為加權的。
2. 圖的表示
圖的正確表示法取決於待解決的問題和圖的型別。
2.1 鄰接矩陣
圖最常見的實現是鄰接矩陣
。
- 通過
二維陣列
來表示頂點
之間的關係
- 每個
節點
和 一個整數
相關,該整數作為陣列的索引
- 索引為
i
的節點 和索引為j
的節點相鄰,則array[i][j] === 1
,不相鄰時array[i][j] === 0
特性:
- 不是
強聯通的圖
(稀疏圖)使用鄰接矩陣表示,矩陣中儲存了很多0
。浪費
了計算機儲存空間
來儲存不存在的邊。 - 圖的定點的數量可能會改變,二維陣列不太靈活
- 查詢具體
兩個頂點
是否是相鄰節點
,比較快
- 查詢某個頂點的
所有相鄰節點
,比較慢
2.2 鄰接表
我們可以使用一種叫鄰接表
的動態資料結構表示圖。領接表
由圖的每個頂點
的相鄰頂點的列表
所組成。相鄰頂點的列表
資料結構可以通過列表(陣列)
、連結串列
、字典
、雜湊表
來表示。
特性:
- 查詢具體
兩個頂點
是否是相鄰節點
,比較慢
,需要獲取所有相鄰節點列表,再查詢具體頂點 - 查詢某個頂點的
所有相鄰節點
,比較快
,直接獲取列表
function Graph() {
// 使用陣列存放所有頂點
let vertices = [];
// 使用字典存放 相鄰頂點 的列表
let adjList = new Map(); // 這裡使用ES6的Map,也就是之前的字典型別資料
// 用於初始化一個頂點:該頂點需要新增到vertices中,並在adjList中建立一個儲存相鄰節點的資料
this.addVertex = (v) => {
vertices.push(v);
adjList.set(v, []);
};
// 實現新增兩個頂點之間的路徑(互相新增,表示無向圖, 只新增一個頂點,則表示有向圖)
this.addEdge =(v, w) => {
adjList.get(v).push(w);
// 有向圖:不需要這條設定
adjList.get(w).push(v);
};
this.toString = function(){
var s = '';
for (var i=0; i<vertices.length; i++){ //{10}
s += vertices[i] + ' -> ';
var neighbors = adjList.get(vertices[i]); //{11}
for (var j=0; j<neighbors.length; j++){ //{12}
s += neighbors[j] + ' ';
}
s += '\n'; //{13}
}
return s;
};
}
// 測試
var graph = new Graph();
var myVertices = ['A','B','C','D','E','F','G','H','I']; //{7}
for (var i=0; i<myVertices.length; i++){ //{8}
graph.addVertex(myVertices[i]);
}
graph.addEdge('A', 'B'); //{9}
graph.addEdge('A', 'C');
graph.addEdge('A', 'D');
graph.addEdge('C', 'D');
graph.addEdge('C', 'G');
graph.addEdge('D', 'G');
graph.addEdge('D', 'H');
graph.addEdge('B', 'E');
graph.addEdge('B', 'F');
graph.addEdge('E', 'I');
console.log(graph.toString);
複製程式碼
結果:
2.3 關聯矩陣
關聯矩陣: 矩陣的行
表示頂點
,矩陣的列
表示邊
。我們使用二維陣列來表示頂點
與邊
的連通性。
如果v 頂點
是邊 e
的入射點(任意一個連線的定點),則array[v][e] === 1
,否則array[v][e] === 0
。
關聯矩陣通常用於
邊的數量
比頂點多
的情況下,以節省空間和記憶體
。3. 圖的遍歷
圖的遍歷方式有兩種:深度優先
(Breadth-First Search,BFS)和廣度優先
(Depth-First Search,DFS)。
圖的用途:尋找特定的頂點、尋找兩個頂點之間的路徑、檢查圖是否聯通、檢查圖是否含有環等等。
圖遍歷演算法思想:
- 必須追蹤
每個第一次
訪問的節點,並且追蹤哪些節點沒有被完全探索
- 兩種演算法需要明確指出
第一個
被訪問的頂點
完全探索一個頂點:要求我們檢視該頂點的每一條邊,對於每一條邊連線而未被訪問的頂點,標註為發現
並新增進待訪問
頂點列表
為保準效率:每個頂點務必訪問至多兩次
。連通圖中每條邊和頂點都會被訪問到
當標註已訪問的頂點時,使用三種顏色反應它們的狀態,這也是為什麼上面說一個頂點務必最多訪問2次的原因:
- 白色: 表示該頂點 未被訪問
- 灰色: 表示該頂點 被訪問但未被探索過
- 黑色: 表示該頂點 被訪問且被探索過
3.1 廣度優先(Breadth-First-Search, BFS)
從上面可以知道:從頂點v開始廣度優先的步驟如下:
- 建立一個佇列Q: 用於存放
被訪問,未被探索【灰色】
的頂點 - 從頂點v開始,將v設定為
灰色
,存放如Q - 如果Q為非空【處理佇列資料】:
- 將w從Q佇列中中取出
- 將w的所有相鄰節點(白色)標記為
灰色
,放入Q佇列 - 將w標記為黑色
// --- 該部分程式碼 直接複製到Graph中
// 將所有的頂點顏色初始化為白色
let initializeColor = () => {
// 儲存所有帶顏色的頂點
let colors = [];
vertices.map(vertice => {
colors[vertice] = 'white';
})
return colors;
};
// BFS
this.bfs = (v, callback) => {
// 存放所有訪問但未被探索過的節點
let queue = new Queue();
let colors = initializeColor();
queue.enqueue(v);
// 迴圈處理所有頂點
while(!queue.isEmpty()) {
let u = queue.dequeue();
colors[u] = 'grey';
let neighbors = adjList.get(u);
neighbors.map(w => {
if (colors[w] === 'white') {
colors[w] = 'grey';
queue.enqueue(w);
}
});
colors[u] = 'black';
if (callback) {
callback(u);
}
}
}
// --- 測試遍歷
function printNode(value){ //{16}
console.log('Visited vertex: ' + value); //{1
}
graph.bfs(myVertices[0], printNode); //{18}
複製程式碼
3.2 通過 BFS 計算最短路徑
通過上面的遍歷,我們可以通過小的改造,新增兩個屬性來記錄 傳入節點v[被計算的開始節點]
和 任意其他節點w[任意其他節點]
之間的距離。
- distance[w]: 存放v節點到w節點的距離。 初始化distance[w] = 0
- predecessors[w]: 存放w節點的前輩。 初始化predecessor[w] = null
this.bfs = (v) => {
console.log(this.toString())
let colors = {}, // 初始化顏色,後續變化中:未被訪問 白色,訪問未被探索 grey, 被探索 black
distance = {}, // 初始化每個頂點離v的距離
predecessors = {}, // 初始化每個頂點的前輩頂點
queue = new Queue(); // 存放待探索的節點
let init = () => {
vertices.map(w => {
colors[w] = 'white';
distance[w] = 0;
predecessors[w] = null;
});
};
// 需要被探索的v
queue.enqueue(v);
init();
while(!queue.isEmpty()) {
let u = queue.dequeue();
colors[u] = 'grey';
let neighbors = adjList.get(u);
neighbors.map(w => {
if (colors[w] === 'white') {
queue.enqueue(w);
distance[w] = distance[u] + 1; // v => w 的具體,通過前輩節點+1
predecessors[w] = u; // 新增元件節點
}
colors[w] = 'grey';
});
colors[u] = 'black';
}
return {
distance,
predecessors
};
}
}
// 獲取所有的定點的最短路徑的所經過的點
this.getRoutes = (v) => {
// 根據bfs獲取 圖中各個祖先節點的關係
let { predecessors } = this.bfs(v);
// 存放所有最短路徑
let allRoute = {};
vertices.map((w) => {
// 除去頂點自己,不需要計算 自己到自己的軌跡
if (w != v) {
// 存放當前 v -> w 的路徑
let route = [w], item = w;
// 從 predecessors 中獲取祖先節點,直到最頂層祖先節點為v,表示v -> w的路徑統計完畢
while (item != v) {
// 獲取自己的祖先頂點
let preW = predecessors[item];
route.push(preW);
item = preW;
}
allRoute[w] = route.reverse().join('-');
}
});
return allRoute;
}
複製程式碼
按照這種演算法:例如查詢A -> D 的距離:看起來有兩種, AD, ACD。 但是在遍歷A的鄰接點時,D已被探索過,因此當探索C頂點時,C的D鄰接點為黑色,表明已經先到達了D頂點,因此不再探索。最終結果為AD路徑。
3.3 DFS 深度優先
深度優先搜尋演算法:將會從第一個指定的頂點開始遍歷圖,沿著路
徑直到
這條路徑的最後
一個頂點
被訪問
,按著原路返回並探索下一條路徑。
訪問途中的v頂點的步驟如下:
- 所有頂點,初始化顏色為
white
- 開始訪問v頂點,標記
v
頂點為grey
。此時記錄頂點v
的發現時間
- 訪問
v
所有未被訪問的鄰接點w
- 訪問
w
,此時記錄w
的前溯點
- 訪問
v
被探索完畢,標記v
為黑色。 此時記錄頂點v
的完成探索時間
// --- 該部分程式碼 直接複製到Graph中
this.dfs =() => {
let colors = {}, // 初始化所有節點顏色: key:頂點, value: grey, white, black
d = [], // 記錄 發現時間 集合
f = [], // 記錄 完成探索時間 集合
p = {}, // 記錄 前溯點
time = 0; // 計時開始時間
// 初始化顏色
let init = () => {
vertices.map(w => {
colors[w] = 'white';
});
};
// 訪問處理
let dfsVisit = (u) => {
// 當被訪問,修改為grey,表示開始訪問,但鄰接點未訪問完畢
colors[u] = 'grey';
// 記錄u的開始時間
d[u] = ++time;
console.log('discovered ' + u);
// 當鄰接點訪問完畢,自己才算完成
let neighbors = adjList.get(u);
neighbors.map(w => {
if (colors[w] === 'white') {
dfsVisit(w);
// 記錄w的前溯點
p[w] = u;
}
});
// 自己訪問完畢,修改為black
colors[u] = 'black';
// 記錄u的完成時間
f[u] = ++time;
console.log('explored ' + u);
};
init();
vertices.map(w => {
if (colors[w] === 'white') {
dfsVisit(w);
}
})
return {
discovery: d,
finished: f,
predecessors: p
};
}
複製程式碼
訪問路徑:
深度搜尋結果: