一.什么是圖?有哪些特性及其使用場景?
由來: 當我們需要表示多對多的關系的時候,就需要使用到圖這種數據結構
定義: 圖是一種數據結構,其中頂點可以是具有零個或多個相鄰元素.兩個頂點之間的連線稱為邊,節點被稱為頂點
常用概念: 無向圖表示頂點之間的連接沒有方向,既可以A->B,也可以B->A,有向圖表示頂點之間的連接有方向,A->B,B->A,表示不同的路徑
圖的創建: 鄰接矩陣(使用二維數組)和鄰接表(使用鏈表)
public class Graph { private List<String> vertexList; // 存儲頂點的集合 private int[][] edges; // 存儲圖對應的鄰接矩陣 private int numberOfEdges; // 表示邊的數量 private boolean[] isVisited; // 是否被訪問過 public static void main(String[] args) { Graph graph = new Graph(5); String[] vertexes = {"A","B","C","D","E"}; // 添加節點 for (String vertex : vertexes) { graph.insertVertex(vertex); } // 添加邊 A-B A-C B-C B-D B-E 用於創建鄰接矩陣 graph.insertWeight(0,1,1); graph.insertWeight(0,2,1); graph.insertWeight(1,2,1); graph.insertWeight(1,3,1); graph.insertWeight(1,4,1); graph.showGraph(); } /** * 構造器 ,初始化頂點,鄰接矩陣,邊的數目,是否訪問過 * @param n */ public Graph(int n) { vertexList = new ArrayList<String> (n); edges = new int[n][n]; isVisited = new boolean[n]; numberOfEdges = 0; } }
/** * 插入頂點 * @param vertex */ public void insertVertex(String vertex){ vertexList.add(vertex); } /** * 添加邊 無向圖 * @param v1 表示第一個頂點對應的下標 * @param v2 表示第二個頂點對應的下標 * @param weight 表示權值 */ public void insertWeight(int v1,int v2,int weight){ edges[v1][v2] = weight; edges[v2][v1] = weight; numberOfEdges++; }
執行結果:
二.圖的遍歷
深度優先遍歷(dfs)
基本思想:
1.從初始訪問的節點出發,初始訪問的節點可能有多個鄰接節點,深度優先遍歷的策略是首先訪問第一個鄰接節點,然后在把這個被訪問過的鄰接節點
作為初始節點,在訪問它的鄰接節點.即每次訪問完當前節點后首先訪問當前節點的第一個鄰接節點
2.這樣的訪問策略是優先縱向的挖掘深入,而不是對所有的節點進行橫向的訪問
3.這是一個遞歸調用的過程
步驟:
1.訪問初始節點v,並將v標記為已訪問
2.查找節點v的第一個鄰接節點w
3.若w存在,繼續執行4,如果不存在,回到第1步,繼續從v的下一個節點繼續
4.若w未被訪問,對w進行深度優先訪問
5.查找節點v的w鄰接節點的下一個鄰接節點,轉到步驟3
/** * 返回節點i對應的數據 * @param i * @return */ public String getValueByIndex(int i){ return vertexList.get(i); } /** * 找到第一個相鄰節點的下標 * @param index * @return */ public int getFirstNeighbor(int index){ for (int j = 0; j < vertexList.size(); j++) { if (edges[index][j] != 0){ return j; } } return -1; } /** * 根據前一個相鄰節點的下標獲取下一個相鄰節點的下標 * @param v1 * @param v2 * @return */ public int getNextNeighbor(int v1,int v2){ for (int j = v2+1; j < vertexList.size(); j++) { if (edges[v1][j] != 0) { return j; } } return -1; } /** * 深度優先遍歷 * @param isVisited * @param i */ private void dfs(boolean[] isVisited,int i){ // 訪問當前的節點 System.out.print(getValueByIndex(i) +"->"); // 將被訪問的節點設置成已訪問 isVisited[i] = true; // 獲取當前節點的相鄰的節點 int w = getFirstNeighbor(i); while (w != -1){ // 只要當前節點的鄰接點不為空 if (!isVisited[w]){ // 如果沒有訪問過 dfs(isVisited, w); //繼續遞歸 } // 繼續從它的下一個鄰接點開始執行 w = getNextNeighbor(i,w); } } public void dfs(){ isVisited = new boolean[vertexList.size()]; for (int i = 0; i < vertexList.size(); i++) { if (!isVisited[i]) { dfs(isVisited, i); } } }
執行結果:
廣度優先遍歷(bfs)
基本思想:
類似於分層搜索的過程,需要使用一個隊列來保存訪問過的節點順序,以便按照這個順序來訪問這些節點的鄰接節點
步驟:
1.訪問初始節點v,並將v標記為已訪問
2.節點v入隊列
3.當隊列非空時,繼續執行,否則算法結束
4.出隊列,取出隊列頭u
5.查找u的第一個鄰接節點w
6.若節點u的的鄰接節點w不存在,轉回步驟3,否則執行步驟7
7.若節點w尚未被訪問,則將w標記為已訪問
8.節點w入隊列
9.查找節點u的繼w的鄰接節點后的下一個鄰接節點,重復步驟6直到隊列為空
/** * 廣度優先遍歷 * @param isVisited * @param i */ private void bfs(boolean[] isVisited, int i){ int u; // 表示隊列頭結點對應的下標 int w; // 鄰接節點w // 用於記錄節點訪問的順序 Queue<Integer> queue = new LinkedList<>(); // 訪問節點,輸出節點的值 System.out.print(getValueByIndex(i) + "->"); // 將已訪問的節點標記為已訪問 isVisited[i] = true; // 將已訪問的節點加入到隊列 queue.add(i); while (!queue.isEmpty()){ // 取出隊列頭節點的下標 u = queue.remove(); // 得到其鄰接節點的下標 w = getFirstNeighbor(u); while (w != -1){ // 如果鄰接節點存在 if(!isVisited[w]){ // 是否已經訪問過該節點 System.out.print(getValueByIndex(w) + "->"); // 訪問該節點 isVisited[w] = true; // 將該節點的狀態設置為已訪問 queue.add(w); // 加入到隊列中 } w = getNextNeighbor(u,w); //以u為前驅節點,找到其下一個節點 } } } public void bfs(){ isVisited = new boolean[vertexList.size()]; for (int i = 0; i < vertexList.size(); i++) { if (!isVisited[i]) { bfs(isVisited,i); } } }
執行結果:
三.求解圖的最小生成樹
什么是最小生成樹?
1.給定一個帶權的無向連通圖,如何選取一顆生成樹,使得樹上所有邊上權的總和為最小,就叫最小生成樹
2.有N個頂點,一定會有N-1條邊,並且包含全部的頂點
如何求得最小生成樹
1.普里姆算法
步驟:
1.設G=(V,E)是聯通網,T=(U,D)是最小生成樹,V,U是頂點集合,E,D是邊的集合
2.若從頂點u開始構建最小生成樹,則從集合V中取出頂點u放入到集合U中,並標記頂點v為已訪問
3.若集合U中的頂點ui和集合U-V的頂點vj之間存在邊,則尋找這些邊權值的最小邊,但不能構成回路,將頂點vj加入集合U中,標記頂點v已訪問
4.重復步驟2,直到集合V和U相等,並且所有的頂點都標記為已訪問,此時D中就有n-1條邊
創建圖對象並初始化
class MGraph{ int vertexes; // 圖的節點個數 char[] data; // 存放頂點坐標 int[][] weight;// 存放邊,即鄰接矩陣 public MGraph(int vertexes) { this.vertexes = vertexes; data = new char[vertexes]; weight = new int[vertexes][vertexes]; } }
class MinTree { /** * 創建圖對象 * @param graph * @param vertexes * @param data * @param weight */ public void createGraph(MGraph graph,int vertexes,char[] data,int[][] weight){ for (int i = 0; i < vertexes; i++) { graph.data[i] = data[i]; for (int j = 0; j < vertexes; j++){ graph.weight[i][j] = weight[i][j]; } } } /** * 顯示圖的鄰接矩陣 * @param graph */ public void show(MGraph graph){ for(int[] link:graph.weight){ System.out.println(Arrays.toString(link)); } } }
public static void main(String[] args) { char[] data = {'A','B','C','D','E','F','G'}; int vertexes = data.length; // 使用10000表示兩條線之間不連通 int[][] weight = { {10000,5,7,10000,10000,10000,2}, {5,10000,10000,9,10000,10000,3}, {7,10000,10000,10000,8,10000,10000}, {10000,9,10000,10000,10000,4,10000}, {10000,10000,8,10000,10000,5,4}, {10000,10000,10000,4,5,10000,6}, {2,3,10000,10000,4,6,10000},}; MinTree minTree = new MinTree(); MGraph graph = new MGraph(vertexes); minTree.createGraph(graph,vertexes,data,weight); minTree.show(graph); }
}
執行結果:
實現普利姆算法,假設從G點開始走
/** * 普利姆算法 * @param graph 圖對象 * @param v 從哪個頂點開始 */ public void prim(MGraph graph,int v){ // 存放訪問過的節點 int[] visited = new int[graph.vertexes]; //初始化visited的值 for (int i = 0; i < graph.vertexes;i++){ visited[i] = 0; } // 把當前的節點標記為已訪問 visited[v] = 1; // h1和h2 記錄兩個頂點的下標 int h1 = -1; int h2 = -1; // 把minWeight設置為一個最大值,表示兩個點不能連通,后續會替換 int minWeight = 10000; for (int k = 1; k < graph.vertexes;k++){ // 因為有vertexes個頂點,所以結束后,會生成vertexes-1條邊,邊的數量 for (int i = 0; i < graph.vertexes;i++){ // i表示已經訪問過的節點 for (int j = 0; j < graph.vertexes;j++){// j表示未訪問過的節點 if (visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight){ // 尋找已訪問的節點和未訪問過的節點間的權值最小的邊 minWeight = graph.weight[i][j]; // 將minWeight的值更新為圖的權重值 h1 = i; // h1更新為已訪問 h2 = j; // h2更新為已訪問 } } } System.out.println("<" + graph.data[h1] + ", "+graph.data[h2] + "> 權值:"+ minWeight); // 把當前的節點設置為已訪問 visited[h2] = 1; // 重置minWeight的值 minWeight = 10000; } }
執行結果:
2.克魯斯卡爾算法
步驟:
1.按照權值從到到小進行排序
2.保證這些邊不構成回路
1.創建圖
public class KruskalDemo { private int edgeNums; // 邊的數量 private char[] vertexes; // 頂點的集合 private int[][] matrix; // 鄰接矩陣 private static final int INF = Integer.MAX_VALUE;// 表示兩點之間不能聯通 public static void main(String[] args) { char[] vertexes = {'A','B','C','D','E','F','G'}; int matrix[][] = { /*A*//*B*//*C*//*D*//*E*//*F*//*G*/ /*A*/ { 0, 12, INF, INF, INF, 16, 14}, /*B*/ { 12, 0, 10, INF, INF, 7, INF}, /*C*/ { INF, 10, 0, 3, 5, 6, INF}, /*D*/ { INF, INF, 3, 0, 4, INF, INF}, /*E*/ { INF, INF, 5, 4, 0, 2, 8}, /*F*/ { 16, 7, 6, INF, 2, 0, 9}, /*G*/ { 14, INF, INF, INF, 8, 9, 0}}; KruskalDemo kruskal = new KruskalDemo(vertexes,matrix); kruskal.show(); } /*初始化頂點和鄰接矩陣*/ public KruskalDemo(char[] vertexes,int[][] matrix){ // 構造方法 int vlen = vertexes.length; //使用復制拷貝的方法初始化頂點 this.vertexes = new char[vlen]; for (int i = 0; i < vertexes.length; i++){ this.vertexes[i] = vertexes[i]; } // 使用復制拷貝的方法,初始化邊(權值) this.matrix = new int[vlen][vlen]; for (int i = 0; i < vertexes.length; i++){ for (int j = 0; j < vertexes.length; j++){ this.matrix[i][j] = matrix[i][j]; } } // 初始化有效邊的數量 for (int i = 0; i < vertexes.length; i++){ // 自己和自己不算有效邊 for (int j = i+1; j < vertexes.length; j++){ if (this.matrix[i][j] != INF){ edgeNums++; } } } } public void show(){ for (int i = 0; i < vertexes.length;i++){ for (int j = 0; j < vertexes.length; j++){ System.out.printf("%d\t",matrix[i][j]); } System.out.println(); } } }
執行結果:
創建邊,根據權值進行升序排列
class Edata implements Comparable<Edata>{ public char start; // 邊的起始點 public char end; // 邊的終點 public int weight; // 邊的權值 public Edata(char start, char end, int weight) { this.start = start; this.end = end; this.weight = weight; } @Override public String toString() { return "Edata [<" + start +","+end+">= "+weight+"]"; } @Override public int compareTo(Edata o) { // 升序排列 return this.weight - o.weight; } }
判斷兩條邊是否是同一個終點,如果不是就加入到樹中,直到樹擴大成一個森林
/** * 返回一個頂點對應的下標值,找不到返回-1 * @param ch * @return */ private int getPosition(char ch){ for (int i = 0; i < vertexes.length;i++){ if (vertexes[i] == ch ){ return i; } } return - 1; } /** * 獲取圖中的邊,放入到Edata數組中, * @return */ private Edata[] getEdges(){ int index = 0; Edata[] edges = new Edata[edgeNums]; for (int i = 0; i < vertexes.length;i++){ for (int j = i+1; j < vertexes.length; j++) { if (matrix[i][j] != INF){ edges[index++] = new Edata(vertexes[i],vertexes[j],matrix[i][j]); } } } return edges; } /** * 獲取下標為i頂點對應的終點,用於判斷兩個頂點的終點是否相同 * @param ends 記錄了各個頂點對應的終點是哪個,在遍歷過程中,逐步形成 * @param i 傳入的頂點對應的下標 * @return 返回這個頂點的終點對應的下標 */ private int getEnd(int[] ends,int i){ while (ends[i] != 0){ i = ends[i]; } return i; }
完成算法
public void kruskal(){ int index = 0; // 表示最后結果的數組索引 int[] ends = new int[edgeNums]; // 用於保存已有最小生成樹中每個頂點的終點 Edata[] rets = new Edata[edgeNums]; // 創建結果數組,保留最后的最小生成樹 Edata[] edges = getEdges(); // 獲取所有邊的集合 Collections.sort(Arrays.asList(edges)); // 排序 // System.out.println("圖的邊的集合=" + Arrays.toString(edges) + " 共"+ edges.length); // 將邊添加到最小生成樹中,同時判斷是否生成回路 for (int i = 0; i < edgeNums; i++){ // 獲取第i條邊的第一個頂點 int p1 = getPosition(edges[i].start); // 獲取第i條邊的第二個頂點 int p2 = getPosition(edges[i].end); // 獲取p1這個頂點在已有最小生成樹中的終點 int m = getEnd(ends,p1); // 獲取p2這個頂點在已有最小生成樹中的終點 int n = getEnd(ends,p2); // 是否構成回路 if (m != n){ // 如果沒有構成回路 ends[m] = n; // 設置m在已有最小生成樹中的終點 rets[index++] = edges[i]; // 將這條邊加入到結果數組 } } System.out.println("最小生成樹為:"); for (int i = 0; i < index; i++){ System.out.println(rets[i]); } }
執行結果:
四.求出圖的最短路徑
1.迪傑斯特拉算法(從單一頂點出發到其他的各個頂點的最小路徑)
步驟:
1.設出發頂點為v,頂點集合V{v1,v2,...vi},v到V中各頂點集合的距離集合Dis,Dis{d1,d2,...di},Dis記錄了v到圖中各個頂點的距離(到自身可以看做是0,v到vi對應的距離是di)
2.從Dis中選擇值最小的di並移出Dis集合,同時移除V集合對應的頂點vi,此時的v到vi即為最短路徑
3.更新Dis集合,比較v到V集合中頂點的距離值,與v通過vi到V集合中頂點的距離值,保留較小的那個(同時也應該更新頂點的前驅節點為vi,表示通過vi達到的)
4.重復步驟2和3,直到最短路徑頂點為目標頂點即可結束
初始化圖
public static void main(String[] args) { char[] vertexes = {'A','B','C','D','E','F','G'}; int[][] matrix = new int[vertexes.length][vertexes.length]; final int N = 65535;// 表示不可以連接 matrix[0]=new int[]{N,5,7,N,N,N,2}; matrix[1]=new int[]{5,N,N,9,N,N,3}; matrix[2]=new int[]{7,N,N,N,8,N,N}; matrix[3]=new int[]{N,9,N,N,N,4,N}; matrix[4]=new int[]{N,N,8,N,N,5,4}; matrix[5]=new int[]{N,N,N,4,5,N,6}; matrix[6]=new int[]{2,3,N,N,4,6,N}; Graph g = new Graph(vertexes,matrix); g.showGraph(); } class Graph { public char[] vertexes; public int[][] matrix; public VisitedVertex vv; public Graph(char[] vertexes, int[][] matrix) { this.vertexes = vertexes; this.matrix = matrix; } public void showGraph(){ for (int[] links:matrix){ System.out.println(Arrays.toString(links)); } } }
執行結果:
初始化前驅節點,已訪問節點和距離
class VisitedVertex { /*記錄各個頂點是否訪問過,1表示已訪問,0表示未訪問*/ public int[] already_arr; /*記錄每一個下標對應值的前一個下標,動態更新*/ public int[] pre_visited; /*記錄出發頂點到各個頂點的距離*/ public int[] dis; /** * * @param length 初始化頂點的個數 * @param index 從哪個頂點開始 */ public VisitedVertex(int length,int index) { already_arr = new int[length]; pre_visited = new int[length]; dis = new int[length]; /*初始化dis數組*/ Arrays.fill(dis,65535); /*設置除法頂點被訪問過*/ this.already_arr[index] = 1; /*設置出發頂點的訪問距離為0*/ this.dis[index] = 0; } /** * 判斷某各頂點是否被訪問過 * @param index * @return */ public boolean in(int index){ return already_arr[index] == 1; } /** * 更新出發頂點到index節點的距離 * @param index * @param len */ public void updateDis(int index,int len){ dis[index] = len; } /** * 更新pre這個頂點的前驅節點為index頂點 * @param pre * @param index */ public void updatePre(int pre,int index){ pre_visited[pre] = index; } /** * 返回出發頂點到index的距離 * @param index * @return */ public int getDis(int index){ return dis[index]; } /** * 繼續選擇並返回新的訪問節點 * @return */ public int updateArr(){ int min = 65535; int index = 0; for (int i = 0; i < already_arr.length;i++){ if (already_arr[i] == 0 && dis[i] < min){ //使用廣度優先的策略,如果初始節點的下一個沒有被訪問,並且可以連通,就選擇下一個節點為出發節點 min = dis[i]; index = i; } } /*更新index頂點被訪問過*/ already_arr[index] = 1; return index; } }
更新周圍頂點和前驅節點的距離,完成算法
public void djs(int index){ vv = new VisitedVertex(vertexes.length,index); /*更新index頂點到周圍頂點的距離和前驅節點*/ update(index); for (int j = 1; j < vertexes.length; j++) { /*選擇並返回新的訪問節點*/ index = vv.updateArr(); /*更新index頂點到周圍頂點的距離和前驅節點*/ update(index); } } /** * 更新index下標頂點周圍頂點的距離和周圍頂點的前驅節點 * @param index */ public void update(int index){ int len = 0; /*遍歷鄰接矩陣的matrix[index]所對應的行*/ for (int i = 0; i < matrix[index].length; i++) { /*出發頂點到index頂點的距離 + 從index到i頂點的距離和*/ len = vv.getDis(index) + matrix[index][i]; /*如果i頂點沒有被訪問過,並且len小於出發頂點到j頂點的距離,就需要更新*/ if (!vv.in(i) && len < vv.getDis(i)){ /*更新i點的前驅節點為index節點*/ vv.updatePre(i,index); /*更新出發頂點到i的距離*/ vv.updateDis(i,len); } } }
執行結果:
2.弗洛伊德算法(求出所有頂點到其他各個頂點的最短距離)
步驟:
1.設頂點vi到vk的最短路徑已知是Lik,頂點vk到vj的最短路徑已知是Lkj,頂點vi到vj的路徑為Lij,那么vi到vj的最短路徑是min((Lik+Lkj),Lij),vk的取值為圖中所有的頂點
則可以獲得vi到vj的最短路徑
2.vi到vk的最短路徑Lik,vj到vk的最短路徑Lkj,可以用同樣的方式獲得(遞歸)
初始化圖
public static void main(String[] args) { char[] vertexes = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' }; //創建鄰接矩陣 int[][] matrix = new int[vertexes.length][vertexes.length]; final int N = 65535; matrix[0] = new int[] { 0, 5, 7, N, N, N, 2 }; matrix[1] = new int[] { 5, 0, N, 9, N, N, 3 }; matrix[2] = new int[] { 7, N, 0, N, 8, N, N }; matrix[3] = new int[] { N, 9, N, 0, N, 4, N }; matrix[4] = new int[] { N, N, 8, N, 0, 5, 4 }; matrix[5] = new int[] { N, N, N, 4, 5, 0, 6 }; matrix[6] = new int[] { 2, 3, N, N, 4, 6, 0 }; FGraph fGraph = new FGraph(vertexes,matrix,vertexes.length); fGraph.show(); } class FGraph{ public char[] vertexes; // 存放各個頂點的數組 public int[][] dis; // 存放各個頂點到其他各頂點的距離 public int[][] pre; // 存放到達目標頂點的前驅節點 /** * 初始化頂點,dis,pre * @param vertexes * @param matrix * @param length */ public FGraph(char[] vertexes, int[][] matrix, int length) { this.vertexes = vertexes; this.dis = matrix; /*對pre數組進行初始化,存放的是前驅節點的下標*/ this.pre = new int[length][length]; for (int i = 0; i < length; i++) { Arrays.fill(pre[i], i); } } public void show() { for(int k = 0; k < dis.length;k++){ // 輸出pre for (int j = 0; j < dis.length; j++) { System.out.print(vertexes[pre[k][j]] + " "); } System.out.println(); // 輸出dis for (int i = 0; i < dis.length; i++) { System.out.print("(" + vertexes[k] + "到"+vertexes[i]+"的最短路徑是: " + dis[k][i] + ") "); } System.out.println(); } } }
實現算法(vki+vkj < vij)
public void floyd(){ int len = 0; // 保存距離 /*對中間節點進行遍歷,k就是中間節點的下標 [A, B, C, D, E, F, G] */ for (int k = 0; k < dis.length; k++) { /*從i頂點開始出發 [A, B, C, D, E, F, G] */ for (int i = 0; i < dis.length; i++) { /*到達j頂點 [A, B, C, D, E, F, G] */ for (int j = 0; j < dis.length; j++) { /*計算出從i出發,經過k中間節點,到達j的距離*/ len = dis[i][k] + dis[k][j]; if (len < dis[i][j]) {/*如果len小於原本的距離*/ /*更新距離表*/ dis[i][j] = len; /*更新前驅節點*/ pre[i][j] = pre[k][j]; } } } } }
執行結果: