圖的基本概念java
根據以前博客數據結構整理 中,咱們能夠知道node
是一種線性數據結構算法
是一種樹結構數組
而這樣一種結構就是一種圖的結構微信
圖的每個點稱爲頂點(Vertex),一般咱們會給頂點標上序號,而這些序號就能夠理解爲索引網絡
固然這是一種抽象後的概念,在實際中,圖能夠表示爲不少,好比社交網絡數據結構
頂點與頂點相連的稱爲邊(Edge)app
而由以上的圖中,因爲各個頂點相鄰的邊是沒有方向的,因此這種圖又被稱爲無向圖(Undirected Graph),在無向圖中,只要兩個頂點相連,那麼不管從哪一個頂點出發均可以到達相鄰的頂點。而相似於下圖的圖是有方向的ide
咱們稱之爲有向圖(Directed Graph),在有向圖中,只可以從起始頂點出發到達方向末端的相鄰頂點,相反則不能夠。因此咱們在考慮現實關係建模的時候,要使用無向圖仍是有向圖,好比地鐵站點之間,不管從哪一個站點出發均可以到達相鄰的同一個線路的站點,因此要使用無向圖。在社交網絡中,若是是微信中,可使用無向圖,由於微信中人與人的關係是好友的關係。但有一些社交工具多是一種關注的關係,而不是好友的關係。好比像下圖中,Anne關注了Bob,而Bob並無關注Anne,這樣咱們就必須使用有向圖來進行建模。函數
若是一個圖中,頂點與頂點的邊只表明一種關係,而沒有任何實際的度量,咱們能夠稱這種圖爲無權圖。而在有一些圖中,它們的邊表明具備必定的度量信息的意義。咱們稱這樣的圖爲有權圖。而這個權指的就是這些度量信息。
因此圖的分類能夠分爲四種:
對於圖的算法有一些只適合於某一類圖,好比最小生成樹算法只適用於有權圖,拓撲排序算法只適用於有向圖,最短路徑算法雖然適用於全部類型的圖,可是對於無向圖和有向圖的方式是不同的。
在無向無權圖中的概念
若是兩個頂點之間有邊,咱們稱爲兩點相鄰
和一個頂點相鄰的全部的邊,咱們成爲點的鄰邊
從一個頂點到另外一個頂點所通過的全部邊,咱們稱爲路徑(Path),好比下圖中從0到6通過了0-1-6,固然從0到6不必定只有這一條路徑。
從一個頂點出發,通過其餘頂點最終回到起始頂點,咱們稱之爲環(Loop),好比下圖中的0-1-2-3-0就是一個環,固然0-1-6-5-4-3-0也是一個環。
對於單個頂點來講也能夠有一條本身到本身的邊,咱們稱爲自環邊,以下圖中的0-0。每兩個相鄰到頂點也可能不僅一條邊,咱們能夠稱爲平行邊,以下圖中的3-4。大多數狀況下自環邊和平行邊沒有意義,通常咱們在處理自環邊和平行邊的時候都是先將其去除,變成沒有自環邊和平行邊的圖。固然也有自環邊和平行邊存在乎義的場景,可是這種狀況比較少。在圖論中,咱們稱沒有自環邊和平行邊的圖爲簡單圖。
固然在一個圖中,並非全部的頂點都必須是相連的
咱們稱在一張圖中能夠相互鏈接抵達的頂點的集合爲聯通份量,因此上面這張圖中就有2個聯通份量。所以一個圖的全部節點不必定所有相連。一個圖多是有多個聯通份量。
這種有環的圖,咱們能夠稱爲有環圖。
像這種沒法找到從一個頂點出發,通過其餘頂點又回到起始頂點的,咱們稱爲無環圖。但它又知足樹的定義的,因此樹是一種無環圖。咱們在圖論中談到樹的定義跟在數據結構中說的樹不徹底是一個概念,圖論中的樹的根節點能夠是任意節點,而數據結構中說的樹每每是固定的一個根節點。雖然樹是一種無環圖,但一個無環圖不必定是樹。
由上圖可知,它是一個有2個聯通份量的圖,但能夠確定的是1個聯通的無環圖是樹。
由該圖咱們能夠看到,右邊的圖跟左邊的圖的頂點是同樣的,區別只在於邊,右邊的圖的邊是左邊的圖的邊的子集。咱們將左邊的圖的一些邊刪除,就能夠獲得右邊的圖。同時右邊的圖仍是一個樹的形狀。那麼這個過程就能夠稱爲聯通圖的生成樹。因爲樹必須是聯通的,因此只有聯通圖纔有可能生成樹。而且該生成樹包含原聯通圖全部的頂點的樹。這個樹也是保障原聯通圖能夠聯通,而且邊數最小的那個圖,因此該樹的邊數爲:V - 1,這裏的V爲頂點數。
可是反過來講,咱們將一個聯通圖刪邊,包含全部的頂點,邊數爲V - 1,卻不必定是聯通圖的生成樹。以下面這個圖,它就已經再也不聯通了,而且產生了環。
那麼一個圖不必定有生成樹,這個圖必須是一個聯通的圖。可是一個圖必定有生成深林。一個聯通圖必定有生成樹。對於不止一個聯通份量的圖,咱們能夠將各個聯通份量生成樹,進而得到生成森林。
在無向圖中,一個頂點的度(degree),就是這個頂點相鄰的邊數,這裏也是在說簡單圖,不考慮自環邊和平行邊。但在一個有向圖中,一個頂點的度的概念不一樣。因此咱們能夠看到下圖中0這個頂點有兩個鄰邊0-一、0-3,因此0這個頂點的度就是2.
接口
public interface Adj { int getV(); int getE(); /** * 是否存在某條邊 * @param v * @param w * @return */ boolean hasEdge(int v,int w); /** * 獲取和頂點相鄰的邊 * @param v * @return */ Collection<Integer> adj(int v); /** * 求一個頂點的度(頂點有多少個鄰邊) * @param v * @return */ int degree(int v); /** * 檢測一個頂點的索引是否有效 * @param v */ void validateVertex(int v); }
實現類
/** * 只支持處理簡單圖 */ public class AdjMatrix implements Adj { //頂點數 private int V; //邊數 private int E; //鄰接矩陣 private int[][] adj; public AdjMatrix(String filename) { File file = new File(filename); try (Scanner scanner = new Scanner(file)) { V = scanner.nextInt(); if (V < 0) { throw new IllegalArgumentException("V必須爲非負數"); } adj = new int[V][V]; E = scanner.nextInt(); if (E < 0) { throw new IllegalArgumentException("E必須爲非負數"); } for (int i = 0;i < E;i++) { int a = scanner.nextInt(); validateVertex(a); int b = scanner.nextInt(); validateVertex(b); if (a == b) { throw new IllegalArgumentException("檢測到自環邊"); } if (adj[a][b] == 1) { throw new IllegalArgumentException("檢測到平行邊"); } adj[a][b] = 1; adj[b][a] = 1; } }catch (IOException e) { e.printStackTrace(); } } @Override public int getV() { return V; } @Override public int getE() { return E; } @Override public boolean hasEdge(int v, int w) { validateVertex(v); validateVertex(w); return adj[v][w] == 1; } @Override public Collection<Integer> adj(int v) { validateVertex(v); List<Integer> res = new ArrayList<>(); for (int i = 0;i < V;i++) { if (adj[v][i] == 1) { res.add(i); } } return res; } @Override public int degree(int v) { return adj(v).size(); } @Override public void validateVertex(int v) { if (v < 0 || v >= V) { throw new IllegalArgumentException("頂點" + v + "無效"); } } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(String.format("V = %d,E = %d\n",V,E)); for (int i = 0;i < V;i++) { for (int j = 0;j < V;j++) { builder.append(String.format("%d ",adj[i][j])); } builder.append("\n"); } return builder.toString(); } public static void main(String[] args) { Adj adjMatrix = new AdjMatrix("/Users/admin/Downloads/g.txt"); System.out.println(adjMatrix); } }
g.txt中的內容(第一行表示有7個頂點,9條邊;第二行到最後表示哪一個頂點與哪一個頂點相連)
7 9 0 1 0 3 1 2 1 6 2 3 2 5 3 4 4 5 5 6
運行結果
V = 7,E = 9
0 1 0 1 0 0 0
1 0 1 0 0 0 1
0 1 0 1 0 1 0
1 0 1 0 1 0 0
0 0 0 1 0 1 0
0 0 1 0 1 0 1
0 1 0 0 0 1 0
時間複雜度和空間複雜度
空間複雜度 O(V^2)
時間複雜度
建圖 O(E)
查看兩個節點是否相鄰 O(1)
求一個點的相鄰節點 O(V)
從鄰接矩陣的空間複雜度O(V^2)來看,若是一個圖有3000個頂點,若是這個圖是一個樹的話,那麼咱們只存儲頂點加邊須要存儲3000 + (3000 - 1)個信息,就是5999個信息,但若是使用鄰接矩陣的話,則須要存儲3000^2個信息,即9000000個信息,咱們能夠看到這個差距是巨大的。
求一個點的相鄰節點的時間複雜度是O(V)的,但其實這個相鄰節點的數量就等於該節點的度,而在鄰接矩陣中,咱們須要掃描所有3000個頂點才能確認一個頂點的相鄰節點,這其實也形成了大量的浪費。若是能找出一個O(degree(v))的算法,那麼將比O(V)要小的多。
稀疏圖和稠密圖
這裏稀疏和稠密是指邊的多少。若是一個圖是一個樹的話,那麼它確定是一個稀疏圖,由於樹是全部圖裏面邊最少的圖。可是一個有環圖並不必定是一個稠密圖。假如一個有環無向圖有3000個頂點,每一個頂點的度爲3,那麼這個圖有3000 * 3 / 2 = 4500條邊。那這個圖最多能夠有3000 * 2999 / 2 = 4498500條邊,它表示每個頂點都跟剩下的2999個頂點相連,因此每個頂點的度爲2999。那麼4500條邊和4498500相比相差了將近1000倍,是一個很大的量級了。
好比說對於上面這個圖,咱們看起來可能很稠密,但其實它只是一個稀疏圖。由於在該圖中度數最大的頂點的度也不過是六、7的樣子。雖然這個圖的頂點個數大概有幾十個,但圖中的邊數比起它所能容納的最多的邊數,實際上是少不少的。
而上圖就是一個典型的稠密圖,雖然圖中只有21個頂點,要遠遠少於以前的稀疏圖,可是它每個頂點都跟剩餘的20個頂點相連,造成的邊數很是多。對於這種每個頂點跟剩餘全部的頂點相連的圖,咱們稱爲徹底圖。在圖論中,咱們處理的大多數問題其實都是稀疏圖。由於在現實中,咱們對具體的問題進行建模的時候,徹底圖或者稠密圖是很是少的。可是稀疏圖和稠密圖之間並無一個黑白分明的界限,沒有固定的標準。但咱們能夠用一個頂點的度/頂點在徹底圖中的度來進行比較,它多是比1/2,1/10,1/100還要少,通常都是稀疏圖。
用鄰接矩陣來表示一個圖的缺點:若是一個圖是比較稀疏的話,那麼它的空間複雜度會比較高。求一個頂點的相鄰頂點所耗費的時間也比較多。而實際生活所處理的圖都是稀疏的。因此鑑於鄰接矩陣的空間複雜度過大,且相鄰節點的時間複雜度較大。咱們就使用鄰接表來表示這個圖
實現類
public class AdjList implements Adj { //頂點數 private int V; //邊數 private int E; //鄰接表 private List<Integer>[] adj; @SuppressWarnings("unchecked") public AdjList(String filename) { File file = new File(filename); try (Scanner scanner = new Scanner(file)) { V = scanner.nextInt(); if (V < 0) { throw new IllegalArgumentException("V必須爲非負數"); } adj = new LinkedList[V]; for (int i = 0;i < V;i++) { adj[i] = new LinkedList<>(); } E = scanner.nextInt(); if (E < 0) { throw new IllegalArgumentException("E必須爲非負數"); } for (int i = 0;i < E;i++) { int a = scanner.nextInt(); validateVertex(a); int b = scanner.nextInt(); validateVertex(b); if (a == b) { throw new IllegalArgumentException("檢測到自環邊"); } if (adj[a].contains(b)) { throw new IllegalArgumentException("檢測到平行邊"); } adj[a].add(b); adj[b].add(a); } }catch (IOException e) { e.printStackTrace(); } } @Override public int getV() { return V; } @Override public int getE() { return E; } @Override public boolean hasEdge(int v, int w) { validateVertex(v); validateVertex(w); return adj[v].contains(w); } @Override public Collection<Integer> adj(int v) { validateVertex(v); return adj[v]; } @Override public int degree(int v) { return adj(v).size(); } @Override public void validateVertex(int v) { if (v < 0 || v >= V) { throw new IllegalArgumentException("頂點" + v + "無效"); } } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(String.format("V = %d,E = %d\n",V,E)); for (int v = 0;v < V;v++) { builder.append(String.format("%d :",v)); adj[v].stream().map(w -> String.format("%d ",w)) .forEach(builder::append); builder.append("\n"); } return builder.toString(); } public static void main(String[] args) { Adj adjList = new AdjList("/Users/admin/Downloads/g.txt"); System.out.println(adjList); } }
運行結果
V = 7,E = 9
0 :1 3
1 :0 2 6
2 :1 3 5
3 :0 2 4
4 :3 5
5 :2 4 6
6 :1 5
空間複雜度 O(V + E)
時間複雜度
建圖 O(E * V) 之因此要乘以V是由於檢測平行邊的時候,須要遍歷頂點鄰接的全部頂點,若是在一個徹底圖下,就要遍歷全部的頂點, 而鏈表是一個線性結構,因此時間複雜度最壞的狀況下會有V這麼大。這也是一個查重的過程。
查看兩點是否相鄰 O(degree(v)),最差狀況下的徹底圖中就是O(V)
求一個點的相鄰節點 O(degree(v)), 最差狀況下的徹底圖中就是O(V)
經過上面的複雜度,咱們能夠看到鄰接表相比於鄰接矩陣,它有兩點不足。
將以上的問題提取出來,就是要快速查重和快速查看兩點是否相鄰
要解決以上的問題,咱們就不能使用鏈表(LinkedList),咱們可使用哈希表(HashSet,時間複雜度O(1)),或者使用紅黑樹(TreeSet,時間複雜度O(log V)).
因爲紅黑樹保證了頂點索引的順序性,咱們使用紅黑樹來進行轉變。而哈希表沒法達到該要求,但因爲哈希表的時間複雜度更低,若是對頂點沒有順序要求,則使用哈希表更優,在一般的解題過程當中推薦使用哈希表。相比哈希表,紅黑樹更節省空間。
實現類
public class AdjSet implements Adj { //頂點數 private int V; //邊數 private int E; //鄰接表 private Set<Integer>[] adj; @SuppressWarnings("unchecked") public AdjSet(String filename) { File file = new File(filename); try (Scanner scanner = new Scanner(file)) { V = scanner.nextInt(); if (V < 0) { throw new IllegalArgumentException("V必須爲非負數"); } adj = new TreeSet[V]; for (int i = 0;i < V;i++) { adj[i] = new TreeSet<>(); } E = scanner.nextInt(); if (E < 0) { throw new IllegalArgumentException("E必須爲非負數"); } for (int i = 0;i < E;i++) { int a = scanner.nextInt(); validateVertex(a); int b = scanner.nextInt(); validateVertex(b); if (a == b) { throw new IllegalArgumentException("檢測到自環邊"); } if (adj[a].contains(b)) { throw new IllegalArgumentException("檢測到平行邊"); } adj[a].add(b); adj[b].add(a); } }catch (IOException e) { e.printStackTrace(); } } @Override public int getV() { return V; } @Override public int getE() { return E; } @Override public boolean hasEdge(int v, int w) { validateVertex(v); validateVertex(w); return adj[v].contains(w); } @Override public Collection<Integer> adj(int v) { validateVertex(v); return adj[v]; } @Override public int degree(int v) { return adj(v).size(); } @Override public void validateVertex(int v) { if (v < 0 || v >= V) { throw new IllegalArgumentException("頂點" + v + "無效"); } } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(String.format("V = %d,E = %d\n",V,E)); for (int v = 0;v < V;v++) { builder.append(String.format("%d :",v)); adj[v].stream().map(w -> String.format("%d ",w)) .forEach(builder::append); builder.append("\n"); } return builder.toString(); } public static void main(String[] args) { Adj adjset = new AdjSet("/Users/admin/Downloads/g.txt"); System.out.println(adjset); } }
運行結果
V = 7,E = 9
0 :1 3
1 :0 2 6
2 :1 3 5
3 :0 2 4
4 :3 5
5 :2 4 6
6 :1 5
空間複雜度 O(V + E)
時間複雜度
建圖 O(E * log V) 相比於鏈表,紅黑樹的查重的時間複雜度就要低的多
查看兩點是否相鄰 O(log V)
求一個點的相鄰節點 O(degree(v)), 最差狀況下的徹底圖中就是O(V)
固然咱們也可使用哈希表來實現
實現類
public class AdjHash implements Adj { //頂點數 private int V; //邊數 private int E; //鄰接表 private Set<Integer>[] adj; @SuppressWarnings("unchecked") public AdjHash(String filename) { File file = new File(filename); try (Scanner scanner = new Scanner(file)) { V = scanner.nextInt(); if (V < 0) { throw new IllegalArgumentException("V必須爲非負數"); } adj = new HashSet[V]; for (int i = 0;i < V;i++) { adj[i] = new HashSet<>(); } E = scanner.nextInt(); if (E < 0) { throw new IllegalArgumentException("E必須爲非負數"); } for (int i = 0;i < E;i++) { int a = scanner.nextInt(); validateVertex(a); int b = scanner.nextInt(); validateVertex(b); if (a == b) { throw new IllegalArgumentException("檢測到自環邊"); } if (adj[a].contains(b)) { throw new IllegalArgumentException("檢測到平行邊"); } adj[a].add(b); adj[b].add(a); } }catch (IOException e) { e.printStackTrace(); } } @Override public int getV() { return V; } @Override public int getE() { return E; } @Override public boolean hasEdge(int v, int w) { validateVertex(v); validateVertex(w); return adj[v].contains(w); } @Override public Collection<Integer> adj(int v) { validateVertex(v); return adj[v]; } @Override public int degree(int v) { return adj(v).size(); } @Override public void validateVertex(int v) { if (v < 0 || v >= V) { throw new IllegalArgumentException("頂點" + v + "無效"); } } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(String.format("V = %d,E = %d\n",V,E)); for (int v = 0;v < V;v++) { builder.append(String.format("%d :",v)); adj[v].stream().map(w -> String.format("%d ",w)) .forEach(builder::append); builder.append("\n"); } return builder.toString(); } public static void main(String[] args) { Adj adjhash = new AdjHash("/Users/admin/Downloads/g.txt"); System.out.println(adjhash); } }
運行結果
V = 7,E = 9
0 :1 3
1 :0 2 6
2 :1 3 5
3 :0 2 4
4 :3 5
5 :2 4 6
6 :1 5
空間複雜度 O(V + E)
時間複雜度
建圖 O(E)
查看兩點是否相鄰 O(1)
求一個點的相鄰節點 O(degree(v)), 最差狀況下的徹底圖中就是O(V),但使用哈希表則沒法按照鄰接頂點的順序來輸出
爲了保證鄰接頂點的順序性,後續以使用紅黑樹爲主。
在以前的數據結構整理 中,咱們知道二分搜索樹的深度優先遍歷爲前序遍歷,中序遍歷和後序遍歷。
咱們來看一下前序遍歷
private void preOrder(Node node) { if (node == null) { return; } list.add(node.getElement()); //遍歷 preOrder(node.getLeft()); //訪問全部子樹,遍歷和node相鄰的其餘node preOrder(node.getRight()); }
在二分搜索樹中,它的節點爲一個Node的對象,而在圖中的節點爲一個頂點的索引值。根據二分搜索樹的遍歷方式,圖的深度優先遍歷也是添加節點,再訪問跟頂點相鄰的其餘頂點,這個是沒有變的。只不過和圖的頂點相鄰的可能不僅兩個頂點,可能有多個,因此咱們要經過adj()方法獲取一個頂點全部相鄰的頂點。但跟二分搜索樹不一樣的是,咱們要判斷哪些頂點被訪問過,要有一個記錄,咱們放在一個數組visited中,若是w這個頂點沒有被訪問過的話,相應的咱們去遞歸調用w這個頂點就行了。之因此在二分搜索樹中不須要考慮節點有沒有被訪問過,是由於樹中沒有環,因此節點的左右子樹是必定沒有被訪問過的,可是在圖中由於有環的存在,因此必定要判斷這個節點是否被訪問過。對於圖的深度優先遍歷的遞歸的終止條件就是對於咱們當前的這個v或者一個相鄰的頂點都沒有,或者它的全部的相鄰的節點都已經被遍歷過了,就不須要繼續遞歸下去了,遞歸函數就會直接終止。換句話說,要麼G.adj(v)爲空,要麼.filter(w -> !visited[w])爲空,則.forEach(this::dfs)都不會繼續執行,遞歸結束。
private void dfs(int v) { visited[v] = true; list.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); //至關於.forEach(w -> dfs(w)) }
在具體調用上,咱們能夠從任意一個頂點出發,好比咱們就從0這個頂點出發。
dfs(0);
咱們用一個全流程圖來講明整個過程
假設有這麼一個圖,它的鄰接表如右邊所示,此時咱們創建一個visited數組
visted 0 1 2 3 4 5 6
依然從0開始遍歷dfs(0),此時0被遍歷過了,咱們找到0的相鄰節點一、2.
visted 0 1 2 3 4 5 6
遍歷結果: 0
因爲1和2都沒有被遍歷,此時dfs(0) -> dfs(1),咱們再找到1的相鄰節點0、三、4
visted 0 1 2 3 4 5 6
遍歷結果: 0 1
因爲0被遍歷過了,此時咱們過濾出來的爲三、4,則咱們開始遍歷3,dfs(0) -> dfs(1) -> dfs(3)
visted 0 1 2 3 4 5 6
遍歷結果: 0 1 3
咱們再找到3的相鄰節點一、二、5,因爲1被遍歷過,此時咱們過濾出來的爲二、5,則咱們開始遍歷2,dfs(0) -> dfs(1) -> dfs(3) -> dfs(2)
visted 0 1 2 3 4 5 6
遍歷結果: 0 1 3 2
咱們再找到2的相鄰節點0、三、6,因爲0、3都被遍歷過,此時咱們過濾出來的爲6,則咱們開始遍歷6,dfs(0) -> dfs(1) -> dfs(3) -> dfs(2) -> dfs(6)
visted 0 1 2 3 4 5 6
遍歷結果: 0 1 3 2 6
咱們再找到6的相鄰節點二、5,因爲2被遍歷過,此時咱們過濾出來的爲5,則咱們開始遍歷5,dfs(0) -> dfs(1) -> dfs(3) -> dfs(2) -> dfs(6) -> dfs(5)
visted 0 1 2 3 4 5 6
遍歷結果: 0 1 3 2 6 5
咱們再找到5的相鄰節點三、6,因爲三、6都被遍歷過,此時咱們過濾出來的結果爲空,這次遞歸結束,返回上層遞歸dfs(6);但因爲6的相鄰節點二、5都被遍歷過了,返回上層遞歸dfs(2);2的相鄰節點0、三、6都被遍歷過了,返回上層遞歸dfs(3);3的相鄰節點一、二、5都被遍歷過了,返回上層遞歸dfs(1);1的相鄰節點爲0、三、4,其中4沒有被遍歷過,因此過濾出來的節點爲4,則咱們開始遍歷4,dfs(0) -> dfs(1) -> dfs(4)
visted 0 1 2 3 4 5 6
遍歷結果: 0 1 3 2 6 5 4
咱們再找到4的相鄰節點爲1,因爲1被遍歷過了,返回上層遞歸dfs(1),1的相鄰節點爲0、三、4都被遍歷過了,返回上層遞歸dfs(0),0的相鄰節點一、2都被遍歷過了,而0又是遞歸調用的頂層,因此所有遞歸結束,所有結果就是[0 1 3 2 6 5 4].
咱們先新建一個h.txt,內容爲
7 8 0 1 0 2 1 3 1 4 2 3 2 6 3 5 5 6
接口
public interface DFS { List<Integer> getPre(); List<Integer> getPost(); }
深度優先遍歷類
/** * 深度優先遍歷 */ public class GraphDFS implements DFS { private Adj G; //訪問過的頂點 private boolean[] visited; private List<Integer> pre = new ArrayList<>(); public GraphDFS(Adj G) { this.G = G; visited = new boolean[G.getV()]; dfs(0); } private void dfs(int v) { visited[v] = true; pre.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return null; } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFS(g); System.out.println(graphDFS.getOrder()); } }
運行結果
[0, 1, 3, 2, 6, 5, 4]
深度優先遍歷的改進
如上圖所示,當咱們的圖有多個聯通份量的時候,上面的算法就沒法遍歷全部的頂點,因此咱們須要對整個深度優先遍歷類進行一個改進
/** * 深度優先遍歷 */ public class GraphDFS implements DFS { private Adj G; //訪問過的頂點 private boolean[] visited; private List<Integer> pre = new ArrayList<>(); public GraphDFS(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { dfs(v); } } } private void dfs(int v) { visited[v] = true; pre.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return null; } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFS(g); System.out.println(graphDFS.getOrder()); } }
修改一下h.txt的內容,使其變成兩個聯通份量的圖
7 6 0 1 0 2 1 3 1 4 2 3 2 6
運行結果
[0, 1, 3, 2, 6, 4, 5]
在以前的二分搜索樹的深度遍歷中分紅了前序遍歷,中序遍歷,後序遍歷
一、前序遍歷
private void preOrder(Node node) { if (node == null) { return; } list.add(node.getElement()); preOrder(node.getLeft()); preOrder(node.getRight()); }
二、中序遍歷
private void inOrder(Node node) { if (node == null) { return; } inOrder(node.getLeft()); list.add(node.getElement()); inOrder(node.getRight()); }
三、後序遍歷
private void postOrder(Node node) { if (node == null) { return; } postOrder(node.getLeft()); postOrder(node.getRight()); list.add(node.getElement()); }
那麼對於二分搜索樹的這個概念一樣適合於圖
private void dfs(int v) { visited[v] = true; list.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); }
在這種在遍歷節點前添加元素的,咱們稱爲深度優先先序遍歷
private void dfs(int v) { visited[v] = true; G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); list.add(v); }
在這種在遍歷節點後添加元素的,咱們稱爲深度優前後序遍歷
如今咱們將這兩中遍歷出來的元素都進行一下存儲
/** * 深度優先遍歷 */ public class GraphDFS implements DFS{ private Adj G; //訪問過的頂點 private boolean[] visited; //深度優先前序遍歷結果 private List<Integer> pre = new ArrayList<>(); //深度優前後續遍歷結果 private List<Integer> post = new ArrayList<>(); public GraphDFS(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { dfs(v); } } } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return post; } private void dfs(int v) { visited[v] = true; pre.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); post.add(v); } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFS(g); System.out.println(graphDFS.getPre()); System.out.println(graphDFS.getPost()); } }
運行結果
[0, 1, 3, 2, 6, 4, 5]
[6, 2, 3, 4, 1, 0, 5]
不過因爲圖的頂點的相鄰節點可能不僅2個,因此它並無二叉樹的中序遍歷。不但圖沒有,多叉樹也沒有,僅僅只有二叉樹纔會有中序遍歷。而圖的深度優前後序遍歷每每在某些條件下會起到很大的做用。
時間複雜度 O(V + E)
咱們來看一下二分搜索樹的非遞歸前序遍歷
public void preOrderNR() { Stack<Node> stack = new Stack<>(); stack.push(root); while (!stack.empty()) { Node cur = stack.pop(); list.add(cur.getElement()); if (cur.getRight() != null) { stack.push(cur.getRight()); } if (cur.getLeft() != null) { stack.push(cur.getLeft()); } } }
那麼圖的非遞歸深度優先遍歷跟二分搜索樹同樣,只不過,咱們須要對於每個節點,用visited數組判斷一下,這個節點是否已經被遍歷過了
/** * 深度優先遍歷 */ public class GraphDFSNoRecursion implements DFS { private Adj G; //訪問過的頂點 private boolean[] visited; //深度優先前序遍歷結果 private List<Integer> pre = new ArrayList<>(); public GraphDFSNoRecursion(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { dfs(v); } } } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return null; } private void dfs(int v) { Stack<Integer> stack = new Stack<>(); stack.push(v); visited[v] = true; while (!stack.empty()) { int cur = stack.pop(); pre.add(cur); G.adj(cur).stream().filter(w -> !visited[w]) .forEach(w -> { stack.push(w); visited[w] = true; }); } } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFSNoRecursion(g); System.out.println(graphDFS.getPre()); } }
運行結果
[0, 2, 6, 3, 1, 4, 5]
在二分搜索樹中,非遞歸的前序遍歷跟遞歸的前序遍歷的結果是同樣的,由於它是嚴格根據左右子樹的規律來進行入棧和出棧(先右後左),不過在圖中,棧的後進先出特性並不能讓其與遞歸的結果順序保持一致。
至於此,若是不保證結果與遞歸結果相同的順序性,固然能夠用棧也能夠用隊列
/** * 深度優先遍歷 */ public class GraphDFSQueue implements DFS { private Adj G; //訪問過的頂點 private boolean[] visited; //深度優先前序遍歷結果 private List<Integer> pre = new ArrayList<>(); public GraphDFSQueue(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { dfs(v); } } } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return null; } private void dfs(int v) { Queue<Integer> queue = new LinkedList<>(); queue.add(v); visited[v] = true; while (!queue.isEmpty()) { int cur = queue.poll(); pre.add(cur); G.adj(cur).stream().filter(w -> !visited[w]) .forEach(w -> { queue.add(w); visited[w] = true; }); } } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFSQueue(g); System.out.println(graphDFS.getPre()); } }
運行結果
[0, 1, 2, 3, 4, 6, 5]
接口
public interface CC { int getCCCount(); }
實現類
/** * 深度優先遍歷 */ public class GraphDFS implements DFS,CC { private Adj G; //訪問過的頂點 private boolean[] visited; //深度優先前序遍歷結果 private List<Integer> pre = new ArrayList<>(); //深度優前後續遍歷結果 private List<Integer> post = new ArrayList<>(); //聯通份量個數 private int cccount = 0; public GraphDFS(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { dfs(v); cccount++; } } } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return post; } @Override public int getCCCount() { return cccount; } private void dfs(int v) { visited[v] = true; pre.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); post.add(v); } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFS(g); System.out.println(graphDFS.getPre()); System.out.println(graphDFS.getPost()); System.out.println(((CC)graphDFS).getCCCount()); } }
運行結果
[0, 1, 3, 2, 6, 4, 5]
[6, 2, 3, 4, 1, 0, 5]
2
接口
public interface CCN extends CC { /** * 檢測兩個頂點是否聯通 * @param v * @param w * @return */ boolean isConnected(int v,int w); /** * 獲取各個聯通份量各自的頂點 * @return */ List<Integer>[] components(); }
實現類
/** * 檢測兩個頂點是否在同一個聯通份量中 */ public class GraphDFSCC implements CCN { private Adj G; //訪問過的頂點 private Integer[] visited; //聯通份量個數 private int cccount = 0; public GraphDFSCC(Adj G) { this.G = G; visited = new Integer[G.getV()]; for (int i = 0;i < visited.length;i++) { visited[i] = -1; } for (int v = 0;v < G.getV();v++) { if (visited[v] == -1) { dfs(v,cccount); cccount++; } } } @Override public int getCCCount() { Stream.of(visited).map(v -> v + " ") .forEach(System.out::print); System.out.println(); return cccount; } @Override public boolean isConnected(int v, int w) { G.validateVertex(v); G.validateVertex(w); return visited[v] == visited[w]; } @Override @SuppressWarnings("unchecked") public List<Integer>[] components() { List<Integer>[] res = new ArrayList[cccount]; for (int i = 0;i < cccount;i++) { res[i] = new ArrayList<>(); } for (int v = 0;v < G.getV();v++) { res[visited[v]].add(v); } return res; } private void dfs(int v, int ccid) { visited[v] = ccid; G.adj(v).stream().filter(w -> visited[w] == -1) .forEach(w -> dfs(w,ccid)); } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); CCN graphDFS = new GraphDFSCC(g); System.out.println(graphDFS.getCCCount()); System.out.println(graphDFS.isConnected(1,6)); List<Integer>[] comp = graphDFS.components(); for (int ccid = 0;ccid < comp.length;ccid++) { System.out.print(ccid + " : "); comp[ccid].stream().map(w -> w + " ") .forEach(System.out::print); System.out.println(); } } }
運行結果
0 0 0 0 0 1 0
2
true
0 : 0 1 2 3 4 6
1 : 5
咱們這條路徑爲可能的一條鏈接路徑,但未必是最短路徑。爲了便於觀察,咱們仍是下面這個2個聯通份量到圖。
接口
public interface Path { /** * 從源到頂點t是否聯通 * @param t * @return */ boolean isConnectedTo(int t); /** * 從源到頂點t所通過的路徑 * @param t * @return */ Collection<Integer> path(int t); }
實現類
/** * 單源路徑 */ public class SingleSourcePath implements Path { private Adj G; //源 private int source; //訪問過的頂點 private boolean[] visited; //路徑前節點 private int[] pre; public SingleSourcePath(Adj G,int source) { G.validateVertex(source); this.G = G; this.source = source; visited = new boolean[G.getV()]; pre = new int[G.getV()]; for (int i = 0;i < pre.length;i++) { pre[i] = -1; } //咱們定義源節點的父節點爲它本身 dfs(source,source); } @Override public boolean isConnectedTo(int t) { G.validateVertex(t); return visited[t]; } @Override public Collection<Integer> path(int t) { List<Integer> res = new ArrayList<>(); if (!isConnectedTo(t)) { throw new IllegalArgumentException("源頂點" + source + "到目標頂點" + t + "未聯通"); } int cur = t; while (cur != source) { res.add(cur); cur = pre[cur]; } res.add(source); Collections.reverse(res); return res; } /** * 深度遍歷 * @param v 須要遍歷的頂點 * @param parent v的上一個頂點(從哪來的) */ private void dfs(int v,int parent) { visited[v] = true; pre[v] = parent; G.adj(v).stream().filter(w -> !visited[w]) .forEach(w -> dfs(w,v)); } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); Path graphDFS = new SingleSourcePath(g,0); System.out.println("0 -> 6 : " + graphDFS.path(6)); System.out.println("0 -> 5 : " + graphDFS.path(5)); } }
運行結果
0 -> 6 : [0, 1, 3, 2, 6]
Exception in thread "main" java.lang.IllegalArgumentException: 源頂點0到目標頂點5未聯通
at com.cgc.cloud.middlestage.user.graph.SingleSourcePath.path(SingleSourcePath.java:43)
at com.cgc.cloud.middlestage.user.graph.SingleSourcePath.main(SingleSourcePath.java:72)
單一源路徑的優化
因爲咱們在單源路徑算法中的深度優先遍歷裏實際上是遍歷了鏈接的全部頂點,而咱們的目標其實只是爲了找出一條從源到目標相連的路徑,其實並不必定要遍歷全部鏈接的頂點,只要找到目標頂點即返回,因此咱們作以下的修改。
接口
public interface TPath { /** * 從源到頂點t是否聯通 * @return */ boolean isConnected(); /** * 從源到頂點t所通過的路徑 * @return */ Collection<Integer> path(); }
實現類
/** * 單源路徑 */ public class OncePath implements TPath { private Adj G; //源 private int source; //目標 private int target; //訪問過的頂點 private boolean[] visited; //路徑前節點 private int[] pre; public OncePath(Adj G, int source,int target) { G.validateVertex(source); G.validateVertex(target); this.G = G; this.source = source; this.target = target; visited = new boolean[G.getV()]; pre = new int[G.getV()]; for (int i = 0;i < pre.length;i++) { pre[i] = -1; } //咱們定義源節點的父節點爲它本身 dfs(source,source); for (boolean e : visited) { System.out.print(e + " "); } System.out.println(); } @Override public boolean isConnected() { return visited[target]; } @Override public Collection<Integer> path() { List<Integer> res = new ArrayList<>(); if (!isConnected()) { throw new IllegalArgumentException("源頂點" + source + "到目標頂點" + target + "未聯通"); } int cur = target; while (cur != source) { res.add(cur); cur = pre[cur]; } res.add(source); Collections.reverse(res); return res; } /** * 深度遍歷 * @param v 須要遍歷的頂點 * @param parent v的上一個頂點(從哪來的) */ private boolean dfs(int v,int parent) { visited[v] = true; pre[v] = parent; if (v == target) { return true; } for (int w : G.adj(v)) { if (!visited[w]) { if (dfs(w,v)) { return true; } } } return false; } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); TPath path1 = new OncePath(g,0,3); System.out.println("0 -> 3 : " + path1.path()); TPath path2 = new OncePath(g,0,5); System.out.println("0 -> 5 : " + path2.path()); } }
運行結果
true true false true false false false
0 -> 3 : [0, 1, 3]
true true true true true false true
Exception in thread "main" java.lang.IllegalArgumentException: 源頂點0到目標頂點5未聯通
at com.cgc.cloud.middlestage.user.graph.OncePath.path(OncePath.java:50)
at com.cgc.cloud.middlestage.user.graph.OncePath.main(OncePath.java:89)
由結果可知,當咱們遍歷到了目標頂點後,其餘的頂點將不會去遍歷,因此結果第一行只有第1、第2、第四個是true,其餘都是false;而若是從源頂點到目標頂點是不聯通的,則會遍歷完整個聯通變量中的節點,因此結果第三行裏只有表明5的節點是false,其餘都是true.
當咱們要判斷一個圖中是否有環的時候,不論這個圖是否有多個聯通份量。咱們在檢測環的時候,最主要要看遍歷的頂點的相鄰節點是不是已經訪問過的,且這個訪問過的節點不是正在被遍歷的這個頂點的父節點。
好比說咱們從0開始遍歷到1,咱們看1的相鄰節點有0、三、4.雖然0是被遍歷過的,可是0是1的父節點,顯然它不知足一個環。因而咱們遍歷到3.
3的相鄰節點爲一、2。1雖然被訪問過,但而1是3的父節點,因而咱們遍歷到了2.
2的相鄰節點有0和6,0是被遍歷過的,且0不是2的父節點,因此咱們此時能夠判定,該圖中有環。
環檢測類
/** * 無向圖的環檢測 */ public class CycleDetection { private Adj G; //訪問過的頂點 private boolean[] visited; //是否有環 @Getter private boolean hasCycle = false; public CycleDetection(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { if (dfs(v,v)) { hasCycle = true; break; } } } } /** * 從頂點v開始,判斷圖中是否有環 * @param v * @param parent * @return */ private boolean dfs(int v,int parent) { visited[v] = true; for (int w : G.adj(v)) { if (!visited[w]) { if (dfs(w,v)) { return true; } }else if (w != parent) { return true; } } return false; } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); CycleDetection graphDFS = new CycleDetection(g); System.out.println(graphDFS.isHasCycle()); } }
運行結果
true
二分圖的概念
雖然根據上圖中,咱們能夠很清晰的看出這是一個二分圖,可是其實它就是下面這張圖
這張圖就不是那麼一眼看出是一個二分圖了。
二分圖檢測類
/** * 二分圖檢測 */ public class BipartitionDetection { private Adj G; //訪問過的頂點 private boolean[] visited; //各頂點的染色 private int[] colors; //是不是一個二分圖 private boolean isBipartite = true; public BipartitionDetection(Adj G) { this.G = G; visited = new boolean[G.getV()]; colors = new int[G.getV()]; for (int i = 0;i < colors.length;i++) { colors[i] = -1; } for (int v = 0;v < G.getV();v++) { if (!visited[v]) { if (!dfs(v,0)) { isBipartite = false; break; } } } } /** * 檢測是不是二分圖 * @param v * @param color * @return */ private boolean dfs(int v,int color) { visited[v] = true; colors[v] = color; for (int w : G.adj(v)) { if (!visited[w]) { if (!dfs(w,1 - color)) { return false; } }else if (colors[w] == colors[v]) { return false; } } return true; } public boolean isBipartite() { return isBipartite; } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); BipartitionDetection graphDFS = new BipartitionDetection(g); System.out.println(graphDFS.isBipartite()); } }
運行結果
true