数据结构与算法 | 图(Graph)
在這之前已經(jīng)寫了數(shù)組、鏈表、二叉樹、棧、隊(duì)列等數(shù)據(jù)結(jié)構(gòu),本篇一起探究一個(gè)新的數(shù)據(jù)結(jié)構(gòu):圖(Graphs )。在二叉樹里面有著節(jié)點(diǎn)(node)的概念,每個(gè)節(jié)點(diǎn)里面包含左、右兩個(gè)子節(jié)點(diǎn)指針;比對(duì)于圖來(lái)說(shuō)同樣有著節(jié)點(diǎn)(node),在圖里也稱為頂點(diǎn)(vertex),頂點(diǎn)之間的關(guān)聯(lián)不在局限于2個(gè)(左、右),一個(gè)頂點(diǎn)可以與任意(0-n個(gè))個(gè)頂點(diǎn)進(jìn)行鏈接,這稱之為邊(edge)。 一般會(huì)把一個(gè)圖里面頂點(diǎn)的集合記作 V ,圖里面邊的集合記作 E,圖也就用 G(V,E) 來(lái)表示。
對(duì)比二叉樹可以看到圖的約束更少,換一個(gè)角度看二叉樹結(jié)構(gòu)是圖的特殊形式,所謂特殊形式指加上更多的限定條件。
圖的分類(Types Of Graph)
可以看到圖的基本的結(jié)構(gòu)非常簡(jiǎn)單,約束也很少,如果在其中加上各種條件約束就可以定義各種類型的圖。
- 約束邊或者頂點(diǎn)個(gè)數(shù)來(lái)分類:
-
零圖(Null graph):只有頂點(diǎn)沒(méi)有邊的圖; -
平凡圖(Trivial graph):只有一個(gè)頂點(diǎn)的圖;
-
- 按照邊是否有指向來(lái)分類:
-
有向圖(Directed Graph):在每個(gè)邊的定義中,節(jié)點(diǎn)都是有序的對(duì)。也就是(A,B)與(B,A)表示不同的邊,一個(gè)代表從A到B方向的邊,一個(gè)代表從B到A方向的邊。 -
無(wú)向圖(Undirected Graph):邊只是代表鏈接,沒(méi)有指向性。(A,B)與(B,A)表示的同樣的邊。
-
- 根據(jù)是否在邊上存儲(chǔ)數(shù)據(jù)分類:
-
權(quán)重圖(Weighted Graph):圖中的邊上附加了權(quán)重或值的圖。這些權(quán)重表示連接兩個(gè)節(jié)點(diǎn)之間的距離、代價(jià)、容量或其他度量。權(quán)重可以是任何數(shù)值,通常用于描述節(jié)點(diǎn)間的關(guān)系特性。
-
還有很多分類在此不一一羅列。每類圖可能還會(huì)有其獨(dú)特的一些特征描述,比如有向圖(Directed Graph)里面,以某頂點(diǎn)作為開始的邊的數(shù)量稱為這個(gè)頂點(diǎn)的入度(Indegree),以某個(gè)頂點(diǎn)作為結(jié)束的邊的數(shù)量稱為這個(gè)頂點(diǎn)的出度(Outdegree)等等。
通過(guò)以上描述,可以感受到圖其實(shí)是非常靈活的數(shù)據(jù)結(jié)構(gòu),同時(shí)它的衍生概念也非常多;初次探究大可不必一一記牢,有個(gè)基本的圖結(jié)構(gòu)知識(shí)體系即可,后續(xù)遇到的時(shí)候再擴(kuò)充圖的知識(shí)體系更為合適。
圖的表達(dá)(Representation of Graphs)
圖的表達(dá)其實(shí)也有多種形式,不過(guò)最基本的形式是:鄰接矩陣(Adjacency Matrix) 與 鄰接表(Adjacency List)
鄰接矩陣(Adjacency Matrix)
鄰接矩陣,所謂“矩陣”具體到代碼其實(shí)就是二維數(shù)組,通過(guò)二維數(shù)組來(lái)表示圖中頂點(diǎn)之間的邊的關(guān)系。二維數(shù)組中的行和列分別代表圖中的頂點(diǎn),矩陣中的值表示頂點(diǎn)之間是否相連或連接的邊的權(quán)重。
且用這種方式來(lái)表示先前示例的圖結(jié)構(gòu),矩陣的值 0代表無(wú)相連邊,1代表有相連邊。如下:
鄰接表(Adjacency List)
鄰接表,所謂“表”指的就是列表 List ,圖中的每個(gè)節(jié)點(diǎn)都有一個(gè)對(duì)應(yīng)的列表,用于存儲(chǔ)與該節(jié)點(diǎn)直接相連的其他節(jié)點(diǎn)的信息。鄰接表中的每個(gè)節(jié)點(diǎn)列表包含了該節(jié)點(diǎn)相鄰節(jié)點(diǎn)的標(biāo)識(shí)符或指針等信息。對(duì)于無(wú)權(quán)圖,通常使用數(shù)組或鏈表來(lái)存儲(chǔ)相鄰節(jié)點(diǎn)的標(biāo)識(shí)符。而對(duì)于帶權(quán)圖,列表中可能還包含了邊的權(quán)重信息。
基本應(yīng)用示例(Basic Examples)
Leetcode 997. 找到小鎮(zhèn)的法官【簡(jiǎn)單】
小鎮(zhèn)里有 n 個(gè)人,按從 1 到 n 的順序編號(hào)。傳言稱,這些人中有一個(gè)暗地里是小鎮(zhèn)法官。
如果小鎮(zhèn)法官真的存在,那么:
小鎮(zhèn)法官不會(huì)信任任何人。
每個(gè)人(除了小鎮(zhèn)法官)都信任這位小鎮(zhèn)法官。
只有一個(gè)人同時(shí)滿足屬性 1 和屬性 2 。
給你一個(gè)數(shù)組 trust ,其中 trusti = ai, bi 表示編號(hào)為 ai 的人信任編號(hào)為 bi 的人。
如果小鎮(zhèn)法官存在并且可以確定他的身份,請(qǐng)返回該法官的編號(hào);否則,返回 -1 。
示例
輸入:n = 2, trust = [1,2]
輸出:2
題目故事背景描述比較多,可以看到 信任的表述 可以用有向圖的邊來(lái)表示,每個(gè)人 用頂點(diǎn) 來(lái)表示,小鎮(zhèn)法官的第1點(diǎn) 代表就是出度為 0,第2點(diǎn) 代表就是 入度為 n-1。 這樣題目就轉(zhuǎn)換為:判斷一個(gè)n個(gè)頂點(diǎn)的有向圖中 是否存在出度為0,入度為n-1的頂點(diǎn) ;存在返回頂點(diǎn)編號(hào),不存在返回 -1。
PS:關(guān)鍵點(diǎn),將復(fù)雜描述的題目,建模成為圖
public int findJudge(int n, int[][] trust) {
int[] outDegree = new int[n+1],inDegree = new int[n+1];
for(int i = 0; i < trust.length; i++){
outDegree[trust[i][0]] ++;
inDegree[trust[i][1]]++;
}
for(int i=1; i<= n; i++)
if(outDegree[i] == 0 && inDegree[i] == (n-1))
return i;
return -1;
}
Leetcode 787. K 站中轉(zhuǎn)內(nèi)最便宜的航班【中等】
有 n 個(gè)城市通過(guò)一些航班連接。給你一個(gè)數(shù)組 flights ,其中 flightsi = fromi, toi, pricei ,表示該航班都從城市 fromi 開始,以價(jià)格 pricei 抵達(dá) toi。
現(xiàn)在給定所有的城市和航班,以及出發(fā)城市 src 和目的地 dst,你的任務(wù)是找到出一條最多經(jīng)過(guò) k 站中轉(zhuǎn)的路線,使得從 src 到 dst 的 價(jià)格最便宜 ,并返回該價(jià)格。 如果不存在這樣的路線,則輸出 -1。
示例
輸入: n = 3, edges = [0,1,100,1,2,100,0,2,500],src = 0, dst = 2, k = 1
輸出: 200
備注:1 <= n <= 100,航班沒(méi)有重復(fù),且不存在自環(huán)
將城市看作是頂點(diǎn),城市-城市之間的航班看作是 有向圖邊,航班的價(jià)格作為邊的權(quán)重,也就完成了題意到圖的建模。考慮到,城市數(shù)量 n < 100, 因此可以采用 鄰接矩陣的方式來(lái)進(jìn)行圖的表達(dá)。
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
// 圖 初始化建模
int[][] map = new int[n][n];
for(int i = 0; i < flights.length; i++){
map[flights[i][0]][flights[i][1]] = flights[i][2];
}
// 其他邏輯
}
以 src 作為 源頂點(diǎn),通過(guò)以 src作為 起始頂點(diǎn)的邊 鏈接到更多的頂點(diǎn)(此時(shí)經(jīng)過(guò) 0個(gè)站中轉(zhuǎn));以這些鏈接到的頂點(diǎn) 為起始點(diǎn),繼續(xù)鏈接到更多的頂點(diǎn)(經(jīng)過(guò) 1個(gè)站中轉(zhuǎn));繼而可以推導(dǎo)到 經(jīng)過(guò) n 個(gè)站中轉(zhuǎn)。這也就是典型的廣度優(yōu)先搜索(BFS),來(lái)遍歷以src作為 源頂點(diǎn)的圖,遍歷代碼如下:
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
// ...
// BFS
Deque<Integer> que = new ArrayDeque<>();
// src 作為起始點(diǎn)
que.offer(src);
// 經(jīng)過(guò) k 個(gè)中轉(zhuǎn)站
for(int i = 0; i <= k && !que.isEmpty(); i++){
int size = que.size();
while( size-- > 0){
int node = que.poll();
for(int j = 0; j < map[node].length; j++){
// map[node][j] == 0 代表 node -> 不相連跳過(guò)
if( map[node][j] == 0) continue;
// ... 這里可以加入遍歷過(guò)程中更多的邏輯
// 進(jìn)入下一輪遍歷
que.offer(j);
}
}
}
// ...
}
考慮題目需要的是 最多經(jīng)過(guò) k 站中轉(zhuǎn)的 最便宜線路,不妨 廣度優(yōu)先遍歷中 用 distSet[] 記錄下 src 可到達(dá)站點(diǎn)的 最低價(jià)格;最后返回 distSet[ dst ] 即可, 這里注意下的是 如果沒(méi)到達(dá),按照題意應(yīng)返回 -1。
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
// ...
int[] distSet = new int[n];
que.offer(src);
for(int i = 0; i <= k && !que.isEmpty(); i++){
// 判斷當(dāng)前最小的 標(biāo)準(zhǔn) 是基于上一輪的遍歷結(jié)果
int[] pre = Arrays.copyOf(distSet,distSet.length);
int size = que.size();
while( size-- > 0){
int node = que.poll();
for(int j = 0; j < map[node].length; j++){
if( map[node][j] == 0) continue;
// distSet[j] == 0 代表之前沒(méi)有到達(dá)過(guò),因此需要 寫入 distSet[j]
// 如果當(dāng)前距離 不之前大,這個(gè)頂點(diǎn)不必進(jìn)行下一輪遍歷
if( distSet[j] != 0 &&
distSet[j] < pre[node] + map[node][j]) continue;
// 記錄最小結(jié)果
distSet[j] = pre[node] + map[node][j] ;
que.offer(j);
}
}
}
// distSet[j] == 0 代表之前沒(méi)有到達(dá)過(guò),返回 -1
return distSet[dst] == 0 ? -1:distSet[dst];
}
這里其實(shí)是 使用 Bellman-Ford 算法的思想進(jìn)行解題;在圖算法領(lǐng)域還有著很多著名的算法,后續(xù)可以整理下更專業(yè)的解讀,這里只是演示個(gè)簡(jiǎn)單的應(yīng)用。
Bellman-Ford 算法,最初由Alfonso Shimbel 1955年提出,但以 Richard Bellman 和 Lester Ford Jr.的名字命名,他們分別于 1958年 和 1956年 發(fā)表了該算法,向前輩致敬。
最后附上完整代碼:
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
int[][] map = new int[n][n];
for(int i = 0; i < flights.length; i++){
map[flights[i][0]][flights[i][1]] = flights[i][2];
}
int[] distSet = new int[n];
Deque<Integer> que = new ArrayDeque<>();
que.offer(src);
for(int i = 0; i <= k && !que.isEmpty(); i++){
int[] pre = Arrays.copyOf(distSet,distSet.length);
int size = que.size();
while( size-- > 0){
int node = que.poll();
for(int j = 0; j < map[node].length; j++){
if( map[node][j] == 0) continue;
if( distSet[j] != 0 &&
distSet[j] < pre[node] + map[node][j]) continue;
distSet[j] = pre[node] + map[node][j] ;
que.offer(j);
}
}
}
return distSet[dst] == 0 ? -1:distSet[dst];
}
歡迎關(guān)注 Java研究者 專欄、博客、公眾號(hào)等。大伙兒的喜歡是創(chuàng)作最大的動(dòng)力。
總結(jié)
以上是生活随笔為你收集整理的数据结构与算法 | 图(Graph)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 一篇文章带你了解接口自动化
- 下一篇: 神经网络入门篇:神经网络的梯度下降(Gr