引言:Re:從零開始的DS生活 圖論學這一篇就夠了,詳細介紹了圖的基本概念;圖的存儲結構,鄰接矩陣,鄰接表;圖的遍歷,廣度度優先遍歷和深度優先遍歷;最小生成樹基本概念,Prim算法,Kruskal算法;最短路徑問題,Dijkstra算法,Floyd算法;拓撲排序。供讀者理解與學習,適合點贊+收藏。有什麼錯誤希望大家直接指出~
友情鏈接:Re:從零開始的DS生活 輕鬆從0基礎寫出鏈表LRU算法、Re:從零開始的DS生活 輕鬆從0基礎實現多種隊列、Re:從零開始的DS生活 輕鬆從0基礎寫出Huffman樹與紅黑樹、Re:從零開始的DS生活 輕鬆和麪試官扯一個小時棧
最小生成樹基本概念,Prim算法,Kruskal算法;最短路徑問題,Dijkstra算法,Floyd算法;
圖的基本概念
圖:由頂點(vertex)和邊(edge)組成的一種結構。頂點的集合V,邊的集合是E,所以圖記爲G=(V,E)兩類信息:頂點信息,邊的信息
無向邊:若頂點vi到vj之間的邊沒有方向,則稱這條邊爲無向邊(Edge), 用無序偶對(vi;vj) 來表示。如果圖中任意兩個頂點之間的邊都是無向邊,則稱該圖爲無向圖(Undirected graphs)。在無向圖中,如果任意兩個頂點之間都存在邊,則稱該圖爲無向完全圖。
有向邊:若從頂點vi到vj的邊有方向,則稱這條邊爲有向邊,也稱爲弧(Arc )。用有序偶<vi, vj>來表示,vi 稱爲弧尾(Tail),vj 稱爲弧頭(Head)。 如果圖中任意兩個頂點之間的邊都是有向邊,則稱該圖爲有向圖(Directed graphs)。有向完全圖定理同上。
圖的權:有些圖的邊或弧具有與它相關的數字,這種與圖的邊或弧相關的數叫做權
連通圖:在無向圖G中,如果從頂點v到頂點v'有路徑,則稱v和v'是連通的。如果對於圖中任意兩個頂點vi、vj∈E, vi 和vj都是連通的,則稱G是連通圖(Connected Graph)。
度:無向圖頂點的邊數叫度,有向圖頂點的邊數叫出度和入度.。
圖的存儲結構,鄰接矩陣,鄰接表;
鄰接矩陣:設圖G有n個頂點,則鄰接矩陣是一個nXn的方陣,定義爲:
無向圖:
有向圖:
帶權的鄰接矩陣:(帶權的圖稱爲網,包括無向網和有向網)使用二維數組存儲網中頂點之間的關係,頂點之間如果有邊或者弧的存在,在數組的相應位置存儲其權值;反之用∞表示。
使用數組存儲圖時需要使用兩個數組-一個數組存放圖中頂點本身的數據一維數組),另外一個數組用於存儲各頂點之間的關係(二維數組。
/**
* 定義圖的結構
*
* @author macfmc
* @date 2020/6/27-8:18
*/
public class Graph {
//節點數目
protected int size = 20;
//定義數組,保存頂點信息
protected String[] nodes;
//定義矩陣保存頂點信息
protected int[][] edges;
protected int[] visit; //遍歷標誌
/**
* vo v1 v2 v3
* vo 0 1 1 1
* v1 1 0 1 0
* v2 1 1 0 1
* v3 1 0 1 0
*/
public Graph() {
size = 4;
//初始化頂點
nodes = new String[size];
for (int i = 0; i < size; i++) {
nodes[i] = String.valueOf(i);
}
//初始化邊
edges = new int[size][size];
edges[0][1] = 1;
edges[0][2] = 1;
edges[0][3] = 1;
edges[1][0] = 1;
edges[1][2] = 1;
edges[2][0] = 1;
edges[2][1] = 1;
edges[2][3] = 1;
edges[3][0] = 1;
edges[3][2] = 1;
}
public static void main(String[] args) {
Graph graph = new Graph();
}
}
鄰接表是圖的一種鏈式存儲結構。使用鄰接表存儲圖時,對於圖中的每一個頂點和它相關的鄰接點都存儲到一個鏈表中。每個鏈表都配有頭結點,頭結點的數據域不爲NULL ,而是用於存儲頂點本身的數據;後續鏈表中的各個結點存儲的是當前頂點的所有鄰接點。
所以,採用鄰接表存儲圖時,有多少頂點就會構建多少個鏈表,爲了便於管理這些鏈表,常用的方法是將所有鏈表的鏈表頭按照一定的順序存儲在一個數組中(也可以用鏈表串起來)。
在鄰接表中,每個鏈表的頭結點和其它結點的組成成分有略微的不同。頭結點需要存儲每個頂點的數據和指向下一個結點的指針, 由兩部分構成:而在存儲鄰接點時,由於各個頂點的數據都存儲在數組中,所以每個鄰接點只需要存儲自己在數組中的位置下標即可。另外還需要一個指向下一個結點的指針。除此之外,如果存儲的是網,還需要一個記錄權值的信息域。所以表頭結點和其它結點的構造分別爲:
info域對於無向圖來說,本身不具備權值和其它相關信息,就可以根據需要將之刪除。
圖的遍歷,廣度度優先遍歷和深度優先遍歷;
深度優先遍歷:
第1步:訪問A。 第2步:A之後有去向“C,D,F”中的三條路。 隨意選擇一個----(A的鄰接點)C。第3步: C之後有去向“B,D”中的兩條路.隨意選擇一個----(C的鄰接點)B。第4步:B已是盡頭,無路可走(撞南牆)。 於是回溯到上一步的C。第5步: C之後去向“B”中的路已走過了。現在只有選擇D。第6步:D之後的路通向A,A已訪問過。於是回溯到C。 第7步:C的兩條路都走過了,不通。繼續回溯到A。 第8步:A的(”C,D”)都走過了,不通。只剩下F可走,走到F。 第9步:F有路(”G”)可走。走到G。
訪問順序是: A -> C -> B -> D -> F -> G -> E
廣度優先遍歷:
第1步:訪問A。第2步:訪問上一步A的鄰接點,有“C,D,F” 。依次訪問C,D,F。第3步: 訪問上一步( C,D,F )的鄰接點,有B,G。第4步:訪問上一步(B,G)的鄰接點,有E。第5步: 訪問上一步E的鄰接點,沒有了,結束。
訪問順序是: A -> C -> D -> F -> B -> G -> E
/**
* @author macfmc
* @date 2020/6/27-9:33
*/
public class GraphCover extends Graph {
private int[] visit = new int[size]; //遍歷標誌,防止死環遍歷
/**
* 深度優先遍歷
* 一條路走到黑,不撞南牆不回頭,對每一個可能的分支路徑深入到不能再深入爲止
*/
public void DeepFirst(int start) {//從第n個節點開始遍歷
visit[start] = 1; //標記爲1表示該頂點已經被處理過
System.out.println("齊天大聖到—>" + this.nodes[start] + "一遊"); //輸出節點數據
for (int i = 0; i < this.size; i++) {
if (this.edges[start][i] == 1 && visit[i] == 0) {
//鄰接點
DeepFirst(i);
}
}
}
private int[] queue = new int[size];
/**
* 廣度優先遍歷
* 廣度優先搜索遍歷圖的過程中以v 爲起始點,由近至遠,依次訪問和v 有路徑相通且路徑長度爲1,2,…的頂點,第一批節點的鄰接點
*/
public void BreadthFirst(int front, int tail) {
int last = tail;
for (int index = front; index <= tail; index++) {
int node = queue[index];
System.out.println("齊天大聖到—>" + this.nodes[node] + "一遊"); //輸出節點數據
//找出所有的鄰接點
for (int i = 0; i < this.size; i++) {
if (this.edges[node][i] == 1 && visit[i] == 0) {
//鄰接點
visit[i] = 1;
queue[++last] = i;
}
}
}
//遍歷下一批節點
if (last > tail) {
BreadthFirst(tail + 1, last);
}
}
public void BreadthFirst(int start) {
queue[0] = start;
visit[start] = 1;
BreadthFirst(0, 0);
}
public static void main(String[] args) {
GraphCover graph0 = new GraphCover();
graph0.DeepFirst(0);
System.out.println("--------------");
GraphCover graph1 = new GraphCover();
graph1.BreadthFirst(0);
}
}
最小生成樹基本概念,Prim算法,Kruskal算法;最短路徑問題,Dijkstra算法,Floyd算法;
最小生成樹
假設通過綜合分析,城市之間的權值如圖2(a)所示,對於(b)的方案中,選擇權值總和爲7的兩種方案最節約經費
簡單得理解就是給定一個帶有權值的連通圖(連通網),如何從衆多的生成樹中篩選出權值總和最小的生成樹,即爲該圖的最小生成樹。
給定一個連通網,求最小生成樹的方法有:普里姆( Prim )算法和克魯斯卡爾( Kruskal )算法
Prim算法(普里姆):
普里姆算法在找最小生成樹時,將頂點分爲兩類,一類 是在查找的過程中已經包含在樹中的(假設爲 A類), 剩下的是另一類(假設爲 B類)對於給定的連通網,起始狀態全部頂點都歸爲B類。在找最小生成樹時,選定任意一個頂點作爲起始點,並將之從B類移至A類;然後找出B類中到A類中的頂點之間權值最小的頂點,將之從B類移至A類,如此重複,直到B類中沒有頂點爲止。所走過的頂點和邊就是該連通圖的最小生成樹。
例如,通過普里姆算法查找圖2(a)的最小生成樹的步驟爲:假如從頂點A出發,頂點B、C、D到頂點A的權值分別爲2、4、2,所以,對於頂點A來說,頂點B和頂點D到A的權值最小,假設先找到的頂點B:繼續分析頂點C和D,頂點C到B的權值爲3,到A的權值爲4;頂點D到A的權值爲2,到B的權值爲無窮大(如果之間沒有直接通路,設定權值爲無窮大的所以頂點D到A的權值最小:最後,只剩下頂點C,到A的權值爲4,到B的權值和到D的權值一樣大,爲3。所以該連通圖有兩個最小生成樹:
例子:此圖結果應爲: A-C, C-F, F-D, C-B, B-E
普里姆算法的運行效率只與連通網中包含的頂點數相關, 而和網所含的邊數無關。所以普里姆算法適合於解決邊稠密的網,該算法運行的時間複雜度爲: O(n^2)
Kruskal算法(克魯斯卡爾):
克魯斯卡爾算法的具體思路是:將所有邊按照權值的大小進行升序排序,然後從小到大判斷,條件爲:如果這個邊不會與之前選擇的所有邊組成迴路,就可以作爲最小生成樹的一部分;反之,捨去。直到具有n個頂點的連通網篩選出來n-1條邊爲止。篩選出來的邊和所有的頂點構成此連通網的最小生成樹。判斷是否會產生迴路的方法爲:在初始狀態下給每個頂點賦予不同的標記,對於遍歷過程的每條邊,其都有兩個頂點,判斷這兩個頂點的標記是否一致,如果一致,說明它們本身就處在一棵樹中,如果繼續連接就會產性迴路;如果不一致,說明它們之間還沒有任何關係,可以連接。
假設遍歷到一條由頂點A和B構成的邊,而頂點A和頂點B標記不同,此時不僅需要將頂點A的標記更新爲頂點B的標記,還需要更改所有和頂點A標記相同的頂點的標記,全部改爲頂點B的標記。
例如,使用克魯斯卡爾算法找圖1的最小生成樹的過程爲:首先,在初始狀態下,對各頂點賦予不同的標記(用顏色區別),如下圖所示:
對所有邊按照權值的大小進行排序,按照從小到大的順序進行判斷,首先是(1, 3),由於頂點1和頂點3標記不同,所以可以構成生成樹的一部分,遍歷所有頂點,將與頂點3標記相同的全部更改爲頂點1的標記,如上圖所示:
其次是(4, 6)邊,兩頂點標記不同,所以可以構成生成樹的一部分,更新所有頂點的標記爲:其次是(2 ,5 )邊,兩頂點標記不同,可以構成生成樹的一部分, 更新所有頂點的標記爲:
其次是(2,5)邊,兩頂點標記不同,可以構成生成樹的一部分,更新所有頂點的標記爲:然後最小的是(3,6)邊,兩者標記不同,可以連接,遍歷所有頂點,將與頂點6標記相同的所有頂點的標記更改爲頂點1的標記:
繼續選擇權值最小的邊,此時會發現,權值爲5的邊有3個,其中(1,4)和(3, 4)各自兩頂點的標記-樣,如果連接會產生迴路,所以捨去,而(2,3)標記不一樣,可以選擇,將所有與頂點2標記相同的頂點的標記全部改爲同頂點3相同的標記:
當選取的邊的數量相比與頂點的數量小1時,說明最小生成樹已經生成。所以最終採用兄魯斯卡爾算法得到的最小生成樹爲(6 )所示
總結:普里姆算法。該算法從頂點的角度爲出發點,時間複雜度爲o(n2) ,更適合與解決邊的綢密度更高的連通網。
克魯斯卡爾算法,從邊的角度求網的最小生成樹,時間複雜度爲O(eloge)。和普里姆算法恰恰相反,更適合於求邊稀疏的網的最小生成樹。
最短路徑問題(Djkstra)
在一個網(有權圖)中, 求-個頂點到另一個頂點的最短路徑的計算方式有兩種:迪傑斯特拉( Dijkstra算法)和弗洛伊德( Floyd )算法。迪傑斯特拉算法計算的是有向網中的某個頂點到其餘所有頂點的最短路徑;弗洛伊德算法計算的是任意兩頂點之間的最短路徑。
迪傑斯特拉( Djkstra算法):迪傑斯特拉算法計算的是從網中一個頂點到其它頂點之間的最短路徑問題。
1、掃描AA鄰接點,記錄鄰接點權重值
2、找出鄰接點裏最小的那個值
/**
* @author macfmc
* @date 2020/6/27-17:57
*/
public class Dijkstra {
//節點數目
protected int size;
//定義數組,保存頂點信息
protected String[] nodes;
//定義矩陣保存頂點信息
protected int[][] edges;
private int[] isMarked;//節點確認--中心標識
private String[] path;//源到節點的路徑信息
private int[] distances;//源到節點的距離
public Dijkstra() {
init();
isMarked = new int[size];
path = new String[size];
distances = new int[size];
for (int i = 0; i < size; i++) {
path[i] = "";
distances[i] = Integer.MAX_VALUE;
}
}
public static void main(String[] args) {
Dijkstra dijkstra = new Dijkstra();
dijkstra.search(3);
}
public void search(int node) {
path[node] = nodes[node];
distances[node] = 0;
do {
flushlast(node);
node = getShort();
} while (node != -1);
}
//1、掃描AA鄰接點,記錄鄰接點權重值
private void flushlast(int node) {
isMarked[node] = 1;
System.out.println(path[node]);
//掃描鄰接點
for (int i = 0; i < size; i++) {
if (this.edges[node][i] > 0) {
//計算AA節點到 i節點的權重值
int distant = distances[node] + this.edges[node][i];
if (distant < distances[i]) {
distances[i] = distant;
path[i] = path[node] + "-->" + nodes[i];
}
}
}
}
// 2、找出鄰接點裏最小的那個值
private int getShort() {
int last = -1;
int min = Integer.MAX_VALUE;
for (int i = 0; i < size; i++) {
if (isMarked[i] == 1) {
continue;
}
if (distances[i] < min) {
min = distances[i];
last = i;
}
}
return last;
}
public void init() {
//初始化頂點
nodes = new String[]{"AA", "A", "B", "C", "D", "E", "F", "G", "H", "M", "K", "N"};
//節點編號-常量
final int AA = 0, A = 1, B = 2, C = 3, D = 4, E = 5, F = 6, G = 7, H = 8, M = 9, K = 10, N = 11;
size = nodes.length;
edges = new int[size][size];
edges[AA][A] = 3;
edges[AA][B] = 2;
edges[AA][C] = 5;
edges[A][AA] = 3;
edges[A][D] = 4;
edges[B][AA] = 2;
edges[B][C] = 2;
edges[B][G] = 2;
edges[B][E] = 3;
edges[C][AA] = 5;
edges[C][E] = 2;
edges[C][B] = 2;
edges[C][F] = 3;
edges[D][A] = 4;
edges[D][G] = 1;
edges[E][B] = 3;
edges[E][C] = 2;
edges[E][F] = 2;
edges[E][K] = 1;
edges[E][H] = 3;
edges[E][M] = 1;
edges[F][C] = 3;
edges[F][E] = 2;
edges[F][K] = 4;
edges[G][B] = 2;
edges[G][D] = 1;
edges[G][H] = 2;
edges[H][G] = 2;
edges[H][E] = 3;
edges[K][E] = 1;
edges[K][F] = 4;
edges[K][N] = 2;
edges[M][E] = 1;
edges[M][N] = 3;
edges[N][K] = 2;
edges[N][M] = 3;
}
}
總結:迪傑斯特拉算法解決的是從網中的一個頂點到所有其它頂點之間的最短路徑算法整體的時間複雜度爲O(n2)。但是如果需要求任意兩頂點之間的最短路徑,使用迪傑斯特拉算法雖然最終雖然也能解決問題,但是大材小用相比之下使用弗洛伊德算法解決此類問題會更合適。
Floyd算法(弗洛伊德):
弗洛伊德的核心思想是:對於網中的任意兩個頂點(例如頂點A到頂點B )來說,之間的最短路徑不外乎有2種情況:一、直接從頂點A到頂點B的弧的權值爲頂點A到頂點B的最短路徑;二、從頂點A開始,經過若干個頂點,最終達到頂點B ,期間經過的弧的權值和爲頂點A到頂點B的最短路徑。
拓撲排序
對有向無環圖進行拓撲排序,只需要遵循兩個原則:
一、在圖中選擇- -一個沒有前驅的頂點V ;
二、從圖中刪除頂點V和所有以該頂點爲尾的弧。
有向無環圖如果頂點本身具有某種實際意義,例如用有向無環圖表示大學期間所學習的全部課程,每個頂點都表示一門課程,有向邊表示課程學習的先後次序,例如要先學《程序設計
進行拓撲排序時,首先找到沒有前驅的頂點V1 ,如(1)所示;在刪除頂點V1及以V1作爲起點的弧後,繼續查找沒有前驅的頂點,此時,V2和V3都符合條件,可以隨機選擇一個,例如(2)所示,選擇V2 , 然後繼續重複以上的操作,直至最後找不到沒有前驅的頂點。
所以,針對圖2來說,拓撲排序最後得到的序列有兩種:
●V1-> V2-> V3-> V4 ●V1-> V3-> V2 -> V4