【尚硅谷_数据结构与算法】十二、算法
文章目錄
- 參考文獻
- 1. 分治算法
- 1.1 基本介紹
- 1.2 基本算法步驟
- 1.3 算法實踐——漢諾塔
- 2. 動態規劃算法
- 2.1 核心思想
- 2.2 算法實踐——背包問題
- 3. KMP算法
- 3.1 應用場景-字符串匹配問題
- 3.2 KMP算法介紹
- 3.2.1 思路分析
- 3.2.2 部分匹配表的產生
- 4. 貪心算法
- 4.1 算法介紹
- 4.2 算法應用——集合覆蓋
- 4.2.1 思路分析
- 4.3 算法應用——錢幣找零
- 5. 普利姆算法
- 5.1 問題提出——修路問題
- 5.2 最小生成樹
- 5.3 普利姆算法介紹
- 5.3.1 普利姆算法步驟
- 5.3.2 算法實踐——修路問題
- 6. 克魯斯卡爾算法
- 6.1 問題提出——公交站問題
- 6.2 算法介紹
- 6.2.1 算法圖解說明
- 6.2.2 算法分析
- 6.2.3 代碼實現
- 7. 迪杰斯特拉算法
- 7.1 問題提出——最短路徑問題
- 7.2 算法介紹
- 7.2.1 算法過程
- 7.2.2 算法實現
- 8. 弗洛伊德算法
- 8.1 算法介紹
- 8.2 算法分析
- 8.3 算法實踐——最短路徑問題
- 9. 馬踏棋盤游戲
- 9.1 游戲介紹
- 9.2 思路分析
參考文獻
1. 分治算法
1.1 基本介紹
- 分治法是一種很重要的算法。字面上的解釋是“分而治之”,就是把一個復雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最后子問題可以簡單的直接求解,原問題的解即子問題的解的合并。這個技巧是很多高效算法的基礎,如排序算法(快速排序,歸并排序),傅立葉變換(快速傅立葉變換)……
- 分治算法可以求解的一些經典問題
- 二分搜索
- 大整數乘法
- 棋盤覆蓋
- 合并排序
- 快速排序
- 線性時間選擇
- 最接近點對問題
- 循環賽日程表
- 漢諾塔
1.2 基本算法步驟
- 分治法在每一層遞歸上都有三個步驟:
- 分解:將原問題分解為若干個規模較小,相互獨立,與原問題形式相同的子問題
- 解決:若子問題規模較小而容易被解決則直接解,否則遞歸地解各個子問題
- 合并:將各個子問題的解合并為原問題的解
1.3 算法實踐——漢諾塔
-
A、B、C三個塔
- 如果只有一個盤,直接A->C
- 如果大于等于兩個盤,就分成兩部分。1. 最下面的一個盤為一部分;2. 上面的所有盤為一部分
- 將上面的所有盤:A->B
- 最下面的一個盤:A->C
- 再將B中的盤:B->C
-
代碼實現
package pers.chh3213.divide_and_conquer.hanoi;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.divide_and_conquer.hanoi* @ClassName : Hanoi.java* @createTime : 2022/2/19 11:28* @Email :* @Description :*/ public class Hanoi {public static void main(String[] args) {hanoiTower(2,'A','B','C');}/*** 漢諾塔* @param num 盤子總數* @param a 塔A* @param b 塔B* @param c 塔C*/public static void hanoiTower(int num,char a,char b, char c){if(num==1) {System.out.printf("第%d個盤子從%s到%s",num,a,c);System.out.println();}else{//如果我們有n>=2情況,我們總是可以看做是兩個盤1.最下邊的一個盤2.上面的所有盤,//1.先把最上面的所有盤A->B, 移動過程會使用到chanoiTower(num-1,a,c,b);//2.把最下面的盤從A移動到CSystem.out.printf("第%d個盤子從%s到%s",num,a,c);System.out.println();//3.再將B中的盤:B->ChanoiTower(num-1,b,a,c);}} } -
遞歸更多講解請參見這篇博客好文
2. 動態規劃算法
2.1 核心思想
- 動態規劃(Dynamic Programming)算法的核心思想是:將大問題劃分為小問題進行解決,從而一步步獲取最優解的處理算法
- 動態規劃算法與分治算法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然后從這些子問題的解得到原問題的解
- 與分治法不同的是,適合于用動態規劃求解的問題,經分解得到子問題往往不是互相獨立的。( 即下一個子階段的求解是建立在上一個子階段的解的基礎上,進行進一步的求解 )
- 動態規劃可以通過填表的方式來逐步推進,得到最優解
2.2 算法實踐——背包問題
-
有一個背包,容量為4磅,現有如下物品
- 要求達到的目標為裝入的背包的總價值最大,并且重量不超出
- 要求裝入的物品不能重復
-
思路分析
背包問題主要是指一個給定容量的背包、若干具有一定價值和重量的物品,如何選擇物品放入背包使物品的價值最大。其中又分01背包和完全背包(完全背包指的是:每種物品都有無限件可用)
這里的問題屬于01背包,即每個物品最多放一個。而無限背包可以轉化為01背包。
算法的主要思想,利用動態規劃來解決。每次遍歷到的第i個物品,根據w[i]和v[i]來確定是否需要將該物品放入背包中。即對于給定的n個物品,設v[i]、w[i]分別為第i個物品的價值和重量,C為背包的容量。再令v[i]j]表示在前i個物品中能夠裝入容量為j的背包中的最大價值。則我們有下面的結果:
//表示填入表的第一行和第一列是 0,主要是為了方便表示物品和容量 (1) v[i][0]=v[0][j]=0; // 當準備加入新增的商品的重量大于當前背包的容量時,就直接使用上一個單元格的裝入策略(裝入物品的價值) (2) 當 w[i]>j 時:v[i][j]=v[i-1][j] // 當準備加入的新增的商品的容量小于等于當前背包的容量, // 裝入的方式: (3) 當 j>=w[i]時:v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}v[i-1][j]:上一個裝法的總價值v[i] : 表示當前商品的價值v[i-1][j-w[i]] : 裝入i-1商品,到剩余空間j-w[i]的總價值-
簡單來說:
- 裝入物品的容量大于背包容量時,直接使用之前裝入背包物品的最大價值
- 裝入物品容量小于等于背包容量時,比較:裝入該物品之前,背包物品的最大價值與裝入該物品后,該物品的價值+剩余容量能放入物品的最大價值。而后選取其中較大者。
-
代碼實現
package pers.chh3213.dynamic_program;import java.util.Arrays;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.dynamic_program* @ClassName : KnapsackProblem.java* @createTime : 2022/2/19 14:47* @Email :* @Description :*/ public class KnapsackProblem {//存儲最大價值private int[][] value;//用于表示物品放入背包的方式private int[][] method;//背包容量private int m;//物品個數private int n;public static void main(String[] args) {//各個物品的重量int[] w = {1,4,3};//物品價值int[] v = {1500,3000,2000};//背包容量int m = 4;//物品個數int n = v.length;KnapsackProblem knapsackProblem = new KnapsackProblem(m, n);knapsackProblem.DP(w,v);}public KnapsackProblem(int m, int n) {this.m = m;this.n = n;value = new int[n+1][m+1];method = new int[n+1][m+1];}/*** 動態規劃* @param w 物品重量數組* @param v 物品價值數組*/public void DP(int[] w, int[]v){///初始化第一行和第一列,這里在本程序中,可以不去處理,因為默認就是0for (int i = 0; i < value.length; i++) {value[i][0]=0;}for (int i = 0; i < value[0].length;i++) {value[0][i]=0;}//不處理第一行,i是從1開始的for (int i = 1; i < value.length; i++) {//不處理第一列,j是從1開始的for(int j = 1; j < value[0].length; j++){//因為我們程序i是從1開始的,因此原來公式中的w[i]修改成w[i-1]if(w[i-1]>j){value[i][j]= value[i-1][j];}else{//背包剩余的容量int remain = j-w[i-1];//如果放入該物品前的最大價值大于放入該物品后的最大價值,就不放入該物品if(value[i-1][j]>v[i-1]+value[i-1][remain]){value[i][j]=value[i-1][j];}else{value[i][j]=v[i-1]+value[i-1][remain];//存入放入方法method[i][j]=1;}}}}//打印放入背包的最大價值for (int[] val:value) {System.out.println(Arrays.toString(val));}//打印價值最大的放法//存放方法的二維數組的最大下標,從最后開始搜索存放方法int i = method.length - 1;int j = method[0].length - 1;while(i > 0 && j > 0) {if(method[i][j] == 1) {System.out.println("將第" + i + "個物品放入背包");//背包剩余容量j -= w[i-1];}i--;}}}
3. KMP算法
3.1 應用場景-字符串匹配問題
- 字符串匹配問題:
有一個字符串 str1= BBC ABCDAB ABCDABCDABDE,和一個子串 str2=ABCDABD。現在要判斷 str1 是否含有 str2, 如果存在,就返回第一次出現的位置, 如果沒有,則返回-1
- 暴力匹配package pers.chh3213.KMP;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.KMP* @ClassName : bruteForce.java* @createTime : 2022/2/19 15:31* @Email :* @Description :暴力解法解決字符串匹配問題*/ public class bruteForce {public static void main(String[] args) {String str1="BBC ABCDAB ABCDABCDABDE";String str2="ABCDABD";int included = isIncluded(str1, str2);if(included!=-1){System.out.println("匹配成功");System.out.println("輸出位置為"+included);}else{System.out.println("匹配失敗");}}public static int isIncluded(String str1,String str2){char[] charArray1 = str1.toCharArray();char[] charArray2 = str2.toCharArray();for (int i = 0; i < charArray1.length; i++) {int index = i;int j;for (j = 0; j < charArray2.length&&index<charArray1.length;) {if(charArray1[index]==charArray2[j]){index++;j++;}else{break;}}if(j==charArray2.length){return i;}}return -1;} }
- 用暴力方法解決的話就會有大量的回潮,每次只移動一位,若是不匹配,移動到下一位接著判斷,浪費了大量,的時間。
3.2 KMP算法介紹
-
KMP 算法(Knuth-Morris-Pratt 算法)是一個著名的字符串匹配算法,效率很高
-
KMP方法算法就利用之前判斷過信息,通過一個next數組,保存模式串中前后最長公共子序列的長度,每次回溯時,通過next數組找到,前面匹配過的位置,省去了大量的計算時間
-
延續章節3.1的問題,要求使用KMP算法完成。
3.2.1 思路分析
首先,用 str1的第一個字符和 str2的第一個字符去比較,不符合,關鍵詞向后移動一位
重復第一步,還是不符合,再后移
一直重復,直到 Str1有一個字符與 Str2的第一個字符符合為止
接著比較字符串和搜索詞的下一個字符,還是符合
遇到 Str1有一個字符與 Str2對應的字符不符合
這時候,想到的是繼續遍歷 str1的下一個字符,重復第 1步。(其實是很不明智的,因為此時 BCD已經比較過了,沒有必要再做重復的工作,一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是” ABCDAB”。KMP 算法的想法是:設法利用這個已知信息,不要把”搜索位置”移回已經比較過的位置,繼續把它向后移,這樣就提高了效率。
怎么做到把剛剛重復的步驟省略掉?可以對 str2計算出一張部分匹配表,這張表的產生在后面介紹。
str2的部分匹配表如下
已知空格與 D不匹配時,前面六個字符” ABCDAB”是匹配的。查表可知,最后一個匹配字符B對應的部分匹配值為 2,因此按照下面的公式算出向后移動的位數:
移動位數 = 已匹配的字符數 - 對應的部分匹配值。因為 6 - 2 等于 4,所以將搜索詞向后移動 4 位
因為空格與C不匹配,搜索詞還要繼續往后移。這時,已匹配的字符數為2(”AB”),對應的部分匹配值為0。所以,移動位數=2-0,結果為2,于是將搜索詞向后移2位。
因為空格與A不匹配,繼續后移一位。
逐位比較,直到發現 C與 D不匹配。于是,移動位數 = 6 - 2,繼續將搜索詞向后移動 4 位
逐位比較,直到搜索詞的最后一位,發現完全匹配,于是搜索完成。如果還要繼續搜索(即找出全部匹配),移動位數 = 7 - 0,再將搜索詞向后移動 7 位,這里就不再重復了
3.2.2 部分匹配表的產生
-
前綴與后綴
-
部分匹配值就是”前綴”和”后綴”的最長的共有元素的長度。
以”ABCDABD”為例:- ”A”的前綴和后綴都為空集,共有元素的長度為 0;
- ”AB”的前綴為[A],后綴為[B],共有元素的長度為 0;
- ”ABC”的前綴為[A, AB],后綴為[BC, C],共有元素的長度 0;
- ”ABCD”的前綴為[A, AB, ABC],后綴為[BCD, CD, D],共有元素的長度為 0;
- ”ABCDA”的前綴為[A, AB, ABC, ABCD],后綴為[BCDA, CDA, DA, A],共有元素為”A”,長度為 1;
- ”ABCDAB”的前綴為[A, AB, ABC, ABCD, ABCDA],后綴為[BCDAB, CDAB, DAB, AB, B],共有元素為”AB”,長度為 2;
- ”ABCDABD”的前綴為[A, AB, ABC, ABCD, ABCDA, ABCDAB],后綴為[BCDABD, CDABD, DABD, ABD, BD,D],共有元素的長度為 0。
-
”部分匹配”的實質是,有時候,字符串頭部和尾部會有重復。比如,”ABCDAB”之中有兩個”AB”,那么它的”部分匹配值”就是2(” AB”的長度)。搜索詞移動的時候,第一個”AB”向后移動4位(字符串長度-部分匹配值),就可以來到第二個”AB”的位置。
-
代碼實現
package pers.chh3213.KMP;import java.util.Arrays;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.KMP* @ClassName : KMPSearch.java* @createTime : 2022/2/19 16:37* @Email :* @Description :*/ public class KMPSearch {public static void main(String[] args) {String str1="BBC ABCDAB ABCDABCDABDE";String str2="ABCDABD";int[] table = getTable(str2);System.out.println("部分匹配表:"+Arrays.toString(table));//[0, 0, 0, 0, 1, 2, 0]int included = kmp(str1, str2,table);if(included!=-1){System.out.println("匹配成功");System.out.println("輸出位置為"+included);//15}else{System.out.println("匹配失敗");}}/*** 得到字符串的部分匹配表* @param matchStr 用于匹配的字符串* @return 部分匹配表*/public static int[] getTable(String matchStr){//部分匹配值的數組int[] partTable = new int[matchStr.length()];//字符串的第一個元素沒有前綴與后綴,部分匹配值為0partTable[0]=0;//i用來指向部分匹配字符串末尾的字符,j用來指向開始的字符for (int i = 1,j=0; i < matchStr.length(); i++) {//當j>0且前綴后綴不匹配時while (j>0&&matchStr.charAt(i)!=matchStr.charAt(j)){//使用部分匹配表中前一個表項的值j = partTable[j-1];}//如果前綴后綴匹配,j向后移,繼續比較if(matchStr.charAt(i)==matchStr.charAt(j)){j++;}//存入匹配值partTable[i]=j;}return partTable;}/*** kmp搜索算法* @param str1 原字符串* @param str2 字串* @param partTable 字串對應的部分匹配表* @return 如果是-1就是沒有匹配到,否則返回第一個匹配的位置,*/public static int kmp(String str1,String str2,int[] partTable){for (int i = 0,j=0; i < str1.length(); i++) {while (j>0&&str1.charAt(i)!=str2.charAt(j)){j=partTable[j-1];}if(str1.charAt(i)==str2.charAt(j)){j++;}if(j==str2.length()){//如果匹配完成,返回第一個字符出現位置return i-j+1;}}return -1;} }
4. 貪心算法
4.1 算法介紹
- 貪心算法(貪心算法)是指在對問題進行求解時,在每一步選擇中都采取最好或者最優(即最有利)的選擇,從而希望能夠導致結果是最好或者最優的算法
- 貪心算法所得到的結果不一定是最優的結果(有時候會是最優解),但是都是相對近似(接近)最優解的結果
4.2 算法應用——集合覆蓋
- 假設存在下面需要付費的廣播臺,以及廣播臺信號可以覆蓋的地區。如何選擇最少的廣播臺,讓所有的地區都可以接收到信號
4.2.1 思路分析
- 遍歷所有的廣播電臺, 找到一個覆蓋了最多未覆蓋的地區的電臺(此電臺可能包含一些已覆蓋的地區,但沒有關系
- 將這個電臺加入到一個集合中(比如 ArrayList), 想辦法把該電臺覆蓋的地區在下次比較時去掉。
- 重復第 1步直到覆蓋了全部的地區
圖解
-
遍歷電臺的覆蓋地區,發現K1覆蓋的地區最多,將K1覆蓋的地區從地區集合中移除。然后將K1放入電臺集合中,并更新覆蓋地區個數
-
遍歷,發現K2覆蓋的地區最多,將K2覆蓋的地區從地區集合中移除。然后將K2放入電臺集合中,并更新覆蓋地區個數
-
遍歷,發現K3覆蓋的地區最多,將K3覆蓋的地區從地區集合中移除。然后將K3放入電臺集合中,并更新覆蓋地區個數
-
遍歷,發現K5覆蓋的地區最多,將K5覆蓋的地區從地區集合中移除。然后將K5放入電臺集合中,并更新覆蓋地區個數。所有區域都被覆蓋,算法結束
-
關鍵代碼示例
/**** @param allAreas 存放的所有地區* @param broadcasts 所有廣播電臺* @return*/public ArrayList<String> greedyMethod(HashSet<String>allAreas, HashMap<String,HashSet<String>>broadcasts){//創建ArrayList,存放選擇的電臺集合ArrayList<String> selects = new ArrayList<>();//定義一個臨時的集合,在遍歷的過程中,//存放遍歷過程中的電臺覆蓋的地區和當前還沒有覆蓋的地區的HashSet<String> tempSet = new HashSet<>();//定義給maxKey,保存在一次遍歷過程中,能夠覆蓋最大未覆蓋的地區對應的電臺的key//如果maxKey不為null,則會加入到selectsString maxKey=null;//如果allAreas不為0,則表示還沒有覆蓋到所有的地區while (allAreas.size()!=0){maxKey=null;//遍歷broadcasts,取出對應 keyfor (String key:broadcasts.keySet()) {//每進行一次fortempSet.clear();//當前這個key能夠覆蓋的地區HashSet<String> areas = broadcasts.get(key);tempSet.addAll(areas);//求出tempSet和allAreas集合的交集,交集會賦給tempSettempSet.retainAll(allAreas);//如果當前這個集合包含的未覆蓋地區的數量,比maxKey指向的集合地區還多//就需要重置maxKeyif(tempSet.size()>0&&(maxKey==null||tempSet.size()>broadcasts.get(maxKey).size())){maxKey = key;}}//maxKey != null, 應該將maxKey加入 selectsif(maxKey!=null){selects.add(maxKey);///將maxKey指向的廣播電臺覆蓋的地區,從allAreas去掉allAreas.removeAll(broadcasts.get(maxKey));}}return selects;} -
完整代碼見于gitee倉庫
4.3 算法應用——錢幣找零
-
假設紙幣金額為1元、5元、10元、20元、50元、100元,用盡可能少的紙幣數量湊成123元。
-
盡可能從大面值一直往下減即可
-
代碼實現
package pers.chh3213.greedy;import java.util.Arrays;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.greedy* @ClassName : MoneyChange.java* @createTime : 2022/2/19 20:04* @Email :* @Description :*/ public class MoneyChange {public static void main(String[] args) {int[] cases = {100,50, 20, 10, 5, 1};//需要從大到小排列int[] arrCases = change(123,cases);System.out.println(Arrays.toString(arrCases));//打印結果for(int i = 0; i<cases.length; i++) {if(arrCases[i] != 0) {System.out.println("需要" + cases[i] + "元的紙幣" + arrCases[i] + "張");}}}/*** 湊錢* @param money 總金額* @param cases 紙幣數組* @return 返回每張錢幣要拿出的數量組成的數組*/public static int[] change(int money,int[]cases){int leftMoney = money;int[] counter = new int[cases.length];if(money<0){System.out.println("輸入金額為負數!!");return null;}while (leftMoney>0){for (int i = 0; i < cases.length; i++) {int count=0;while(leftMoney-cases[i]>=0){count++;leftMoney = leftMoney-cases[i];}counter[i]=count;}}return counter;} }
5. 普利姆算法
5.1 問題提出——修路問題
- 勝利鄉有7個村莊(A, B,C,D, E, F,G) ,現在需要修路把7個村莊連通,各個村莊的距離用邊線表示(權),比如A-B距離5公里。問:如何修路保證各個村莊都能連通,并且總的修建公路總里程最短?
5.2 最小生成樹
- 修路問題本質就是就是最小生成樹問題
- 最小生成樹(Minimum Cost Spanning Tree),簡稱 MST。給定一個帶權的無向連通圖,如何選取一棵生成樹,使樹上所有邊上權的總和為最小,這叫最小生成樹。
- N個頂點,一定有N-1條邊
- 包含全部頂點
- N-1條邊都在圖中
- 求最小生成樹的算法主要是普里姆算法和克魯斯卡爾算法
5.3 普利姆算法介紹
- 普利姆(Prim)算法求最小生成樹,也就是在包含n個頂點的連通圖中,找出只有(n-1)條邊包含所有n個頂點的連通子圖,也就是所謂的極小連通子圖。
5.3.1 普利姆算法步驟
- 設G=(V,E)G=(V, E)G=(V,E)是具有n個頂點的連通網, T=(U,TE)T=(U, TE)T=(U,TE)是G的最小生成樹, T的初始狀態為U=u0(u0∈V)U={u0} (u0\in V)U=u0(u0∈V), TE={}。重復執行下述操作:在所有u∈Uu\in Uu∈U, v∈V?Uv\in V-Uv∈V?U的邊中找一條代價最小的邊(u,v)(u,v)(u,v)并入集合TE,同時v并入U,直至U=V。
- Prim算法的基本思想用偽代碼描述如下;
5.3.2 算法實踐——修路問題
-
MGraph類代碼
package pers.chh3213.prim;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.prim* @ClassName : MGraph.java* @createTime : 2022/2/20 9:55* @Email :* @Description :圖類的創建*/ public class MGraph {int vertex;char[] data;int[][] weight;public MGraph(int vertex) {//表示圖的頂點個數this.vertex = vertex;//存放頂點數據data = new char[vertex];//存放邊,就是我們的鄰接矩陣weight = new int[vertex][vertex];} } -
MinTree類代碼
package pers.chh3213.prim;import java.util.Arrays;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.prim* @ClassName : MinTree.java* @createTime : 2022/2/20 9:58* @Email :* @Description :根據村莊的圖創建最小生成樹*/ public class MinTree {//圖對象MGraph graph;//圖的各個頂點的值char[] data;//圖對應的頂點int vertex;//圖的鄰接矩陣int[][]weight;public MinTree(char[] data, int[][] weight) {this.data = data;this.weight = weight;this.vertex = data.length;this.graph = new MGraph(this.vertex);}/*** 創建圖的鄰接矩陣*/public void createGraph(){int i,j;for (i = 0; i < vertex; i++) {graph.data[i]=data[i];for (j=0;j<vertex;j++){graph.weight[i][j]=weight[i][j];}}}/*** 顯示圖的鄰接矩陣*/public void showGraph(){for (int[] link: graph.weight) {System.out.println(Arrays.toString(link));}}/*** 編寫prim算法,得到最小生成樹* @param v 表示從圖的第幾個頂點開始生成*/public void prim(int v){///visited[]標記結點(頂點)是否被訪問過int[] visited = new int[graph.vertex];//把當前這個結點標記為已訪問visited[v]=1;//h1和h2記錄兩個頂點的下標int h1 = -1;int h2 = -1;//將minWeight初始成一個大數,后面在遍歷過程中,會被替換,int minWeight = 10000;//因為有graph.verxs頂點,普利姆算法結束后,有graph.verxs-1邊for (int i = 1; i < graph.vertex; i++) {//確定每一次生成的子圖,和哪個結點的距離最近for (int j = 0; j < graph.vertex ; j++) {//j結點表示被訪問過的結點for (int k = 0; k < graph.vertex; k++) {//k結點表示未被訪問過的結點if(visited[j]==1&&visited[k]==0&&graph.weight[j][k]<minWeight){minWeight = graph.weight[j][k];h1=j;h2=k;}}}//找到一條邊是最小System.out.println("邊<" + graph.data[h1] +"," + graph.data[h2] + ">權值 :" + minWeight);//將當前這個結點標記為已經訪問visited[h2]=1;//minweight 重新設置為最大值10000minWeight=10000;}}} -
PrimDemo類代碼
package pers.chh3213.prim;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.prim* @ClassName : PrimDemo.java* @createTime : 2022/2/20 9:54* @Email :* @Description :*/ public class PrimDemo {public static void main(String[] args) {char[] data = new char[]{'A','B','C','D','E','F','G'};//鄰接矩陣的關系使用二維數組表示,10000這個大數,表示兩個點不聯通int[][] weight = new int[][]{{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(data, weight);minTree.createGraph();minTree.showGraph();//普利姆算法生成最小生成樹minTree.prim(0);} } -
PrimDemo代碼運行結果為:
/* 邊<A,G>權值 :2 邊<G,B>權值 :3 邊<G,E>權值 :4 邊<E,F>權值 :5 邊<F,D>權值 :4 邊<A,C>權值 :7*/ -
代碼倉庫見于gitee
6. 克魯斯卡爾算法
6.1 問題提出——公交站問題
6.2 算法介紹
6.2.1 算法圖解說明
-
以城市公交站問題來圖解說明克魯斯卡爾算法的原理和步驟:
-
對于如上圖G4所示的連通網可以有多棵權值總和不相同的生成樹。
-
假設用數組R保存最小生成樹結果
-
第1步:將邊<E,F>加入R中。邊<E,F>的權值最小,因此將它加入到最小生成樹結果R中。
-
第2步:將邊<C,D>加入R中。上一步操作之后,邊<C,D>的權值最小,因此將它加入到最小生成樹結果R中。
-
第3步:將邊<D,E>加入R中。上一步操作之后,邊<D,E>的權值最小,因此將它加入到最小生成樹結果R中。
-
第4步:將邊<B,F>加入R中。上一步操作之后,邊<C,E>的權值最小,但<C,E>會和已有的邊構成回路:因此,跳過邊<C,E>。同理,跳過邊<C,F>。將邊<B,F>加入到最小生成樹結果R中。
-
第5步:將邊<E,G>加入R中。
上一步操作之后,邊<E,G>的權值最小,因此將它加入到最小生成樹結果R中。 -
第6步:將邊<A,B>加入R中。上一步操作之后,邊<F,G>的權值最小,但<F,G>會和已有的邊構成回路:因此,跳過邊<F,G>。同理,跳過邊<B,C>。將邊<A,B>加入到最小生成樹結果R中。
-
此時,最小生成樹構造完成!它包括的邊依次是: <E,F> <C,D> <D,E> <B,F> <E,G><A,B>。
6.2.2 算法分析
克魯斯卡爾算法重點需要解決的以下兩個問題:
-
問題一:對圖的所有邊按照權值大小進行排序。采用排序算法進行排序即可。
-
問題二:將邊添加到最小生成樹中時,怎么樣判斷是否形成了回路。處理方式是:記錄頂點在"最小生成樹"中的終點,頂點的終點是"在最小生成樹中與它連通的最大頂點"。
然后每次需要將一條邊添加到最小生存樹時,判斷該邊的兩個頂點的終點是否重合,重合的話則會構成回路。 -
舉例
- 在將<E,F> <C,D> <D,E>加入到最小生成樹R中之后,這幾條邊的頂點就都有了終點:
(01) C的終點是F。
(02) D的終點是F。
(03) E的終點是F。
(04) F的終點是F - 關于終點的說明:
- 就是將所有頂點按照從小到大的順序排列好之后:某個頂點的終點就是"與它連通的最大頂點"。
- 因此,接下來,雖然<C,E>是權值最小的邊。但是C和E的終點都是F,即它們的終點相同,因此,將<C,E>加入最小生成樹的話,會形成回路。這就是判斷回路的方式。也就是說,我們加入的邊的兩個頂點不能都指向同一個終點,否則將構成回路。
- 在將<E,F> <C,D> <D,E>加入到最小生成樹R中之后,這幾條邊的頂點就都有了終點:
6.2.3 代碼實現
參考gitee倉庫
7. 迪杰斯特拉算法
7.1 問題提出——最短路徑問題
- 戰爭時期,勝利鄉有7個村莊(A, B, C, D, E, F,G),現在有六個郵差,從G點出發,需要分別把郵件分別送到A, B, C,D,E,F六個村莊, 各個村莊的距離用邊線表示(權),比如A-B距離5公里, 問:如何計算出G村莊到其它各個村莊的最短距離?如果從其它點出發到各個點的最短距離又是多少?
7.2 算法介紹
- 迪杰斯特拉(Dijkstra)算法是典型最短路徑算法,用于計算一個結點到其他結點的最短路徑。它的主要特點是以起始點為中心向外層層擴展(廣度優先搜索思想),直到擴展到終點為止。
7.2.1 算法過程
7.2.2 算法實現
代碼實現來源于 參考資料4.
package pers.chh3213.dijkstra;import java.util.ArrayList;public class Dijkstra {// 約定 10000 代表距離無窮大public static void main(String[] args) {// 頂點char[] vertexes = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };int[][] weight = { // 圖的鄰接矩陣/*A*//*B*//*C*//*D*//*E*//*F*//*G*//*A*/{0, 5, 7, INF, INF, INF, 2},/*B*/{5, 0, INF, 9, INF, INF, 3},/*C*/{7, INF, 0, INF, 8, INF, INF},/*D*/{INF, 9, INF, 0, INF, 4, INF},/*E*/{INF, INF, 8, INF, 0, 5, 4},/*F*/{INF, INF, INF, 4, 5, 0, 6},/*G*/{2, 3, INF, INF, 4, 6, 0}};// 起點下標int source = 6;// 使用迪杰斯特拉查找最短路徑int[] dis = dijkstra(source, vertexes, weight);// 輸出最短路徑長度for (int i=0; i<dis.length; i++){System.out.println(vertexes[source] + "->" + vertexes[i] + " = " + dis[i]);}}private final static int INF = 10000;/*** 迪杰斯特拉算法求解最短路徑問題* @param source 起點下標* @param vertexes 頂點集合* @param weight 鄰接矩陣* @return int[] 起點到各頂點最短路徑距離*/public static int[] dijkstra(int source, char[] vertexes, int[][] weight){// 記錄起點到各頂點的最短路徑長度,如 dis[2] 表示起點到下標為 2 的頂點的最短路徑長度int[] dis;// 存儲已經求出到起點最短路徑的頂點。ArrayList<Character> S = new ArrayList<>();/* 初始化起點 */dis = weight[source];S.add(vertexes[source]);/* 當 S 集合元素個數等于頂點個數時,說明最短路徑查找完畢 */while(S.size() != vertexes.length){int min = INF;// 記錄已經求出最短路徑的頂點下標int index = -1; /* 從 V-S 的集合中找距離起點最近的頂點 */for (int j=0; j<weight.length; j++){if (!S.contains(vertexes[j]) && dis[j] < min){min = weight[source][j];index = j;}}// 更新起點到該頂點的最短路徑長度dis[index] = min;// 將頂點加入到 S 集合中,即表明該頂點已經求出到起點的最小路徑S.add(vertexes[index]);/* 更新起點經過下標為 index 的頂點到其它各頂點的最短路徑 */for (int m=0; m<weight.length; m++){if (!S.contains(vertexes[m]) && dis[index] + weight[index][m] < dis[m]){dis[m] = dis[index] + weight[index][m];}}}return dis;} }8. 弗洛伊德算法
8.1 算法介紹
8.2 算法分析
8.3 算法實踐——最短路徑問題
-
戰爭時期,勝利鄉有7個村莊(A, B, C, D, E, F,G),現在有六個郵差,從G點出發,需要分別把郵件分別送到A, B, C,D,E,F六個村莊, 各個村莊的距離用邊線表示(權),比如A-B距離5公里, 問:如何計算出G村莊到其它各個村莊的最短距離?如果從其它點出發到各個點的最短距離又是多少?
-
代碼實現
package pers.chh3213.floyd;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.floyd* @ClassName : FloydAlgo.java* @createTime : 2022/2/20 15:14* @Email :* @Description :*/ public class FloydAlgo {public static void main(String[] args) {int INF = 10000;// 頂點char[] vertexes = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };int[][] dis = { // 圖的鄰接矩陣/*A*//*B*//*C*//*D*//*E*//*F*//*G*//*A*/{0, 5, 7, INF, INF, INF, 2},/*B*/{5, 0, INF, 9, INF, INF, 3},/*C*/{7, INF, 0, INF, 8, INF, INF},/*D*/{INF, 9, INF, 0, INF, 4, INF},/*E*/{INF, INF, 8, INF, 0, 5, 4},/*F*/{INF, INF, INF, 4, 5, 0, 6},/*G*/{2, 3, INF, INF, 4, 6, 0}};FloydAlgo floydAlgo = new FloydAlgo();int[][] newDis = floydAlgo.floyd(dis);floydAlgo.show(vertexes,newDis);}/*** 弗洛伊德算法* @param dis 從各個頂點出發到其它頂點的距離,最后的結果,也是保留在該數組*/public int[][] floyd(int[][] dis){//遍歷中轉頂點for (int k = 0; k < dis.length; k++) {//從i頂點出發for (int i = 0; i < dis.length; i++) {//到達j頂點for (int j = 0; j < dis.length; j++) {if(dis[i][k]+dis[k][j]<dis[i][j]){dis[i][j]=dis[i][k]+dis[k][j];}}}}return dis;}/*** 輸出每一對頂點之間的最短距離* @param vertex 頂點數組* @param dis 距離數組*/public void show(char[] vertex,int[][] dis){for (int i = 0; i < dis.length; i++) {for (int j = 0; j < dis[i].length; j++) {System.out.print(vertex[i]+"->"+vertex[j]+"="+dis[i][j]+"\t");}System.out.println();}} } -
運行結果為
9. 馬踏棋盤游戲
9.1 游戲介紹
規則(馬走日字)進行移動。要求每個方格只進入一次,走遍棋盤上全部64個方格
9.2 思路分析
馬踏棋盤問題實際上是圖的深度優先搜索(DFS)的應用。
- 假設以 V 為起點,首先找出在指定規則下 V 點下一步可能的落點。
- 在下一步的可能的落點中選擇一個點(假設是 U 點),然后走到 U 點。
- 再以 U 點為起點,找出指定規則下 U 點下一步可能的落點。
- 在下一步可能的落點中選擇一個點(假設是 W 點),然后走到 W 點。
- 如此循環下去,直至走完了所有的格子。
需要注意的是,在下一步可能的落點中選擇一個點這個操作具體怎么進行呢?是隨機選擇一個點的嗎?
并不是隨機的,這里面用到了貪心算法:為了讓之后的選擇盡可能少一點,一般會在下一步可能的落點選項中優先選擇這樣的一個點,這個點的特點是它的下一步可能的落點的選擇最少。
-
代碼實現(代碼中的Point類為java庫,在包package java.awt下)
package pers.chh3213.horse_chessboard;import java.awt.*; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.horse_chessboard* @ClassName : HorseChessboard.java* @createTime : 2022/2/20 15:48* @Email :* @Description :*/ public class HorseChessboard {//棋盤的行數private static int X=8;//棋盤的列數private static int Y=8;//創建一個數組,標記棋盤的各個位置是否被訪問過private static boolean[][] isVisited;//1使用一個屬性,標記是否棋盤的所有位置都被訪問// 如果為 true,表示成功private static boolean isFinished;public static void main(String[] args) {//馬兒初始位置的行,從1開始編號,int row = 1;//馬兒初始位置的列,從1開始編號,int column=1;//創建棋盤int[][] chessboard = new int[X][Y];//初始值都是falseisVisited = new boolean[X][Y];///測試一下耗時long start = System.currentTimeMillis();traversalChessboard(chessboard, row - 1, column - 1, 1);long end = System.currentTimeMillis();System.out.println("共耗:"+ (end - start) +"毫秒");for (int[] item:chessboard) {System.out.println(Arrays.toString(item));}}/*** 完成騎士周游問題的算法* @param chessboard 棋盤* @param row 當前位置的行,從0開始* @param col 當前位置的列,從0開始* @param step 第幾步,初始位置是第一步*/public static void traversalChessboard(int[][] chessboard,int row,int col, int step){chessboard[row][col]=step;isVisited[row][col]=true;ArrayList<Point> nextPoints = next(new Point(row, col));sort(nextPoints);while (!nextPoints.isEmpty()){// 排序后,移動到的下一個點的下一步的可選項個數時最小的Point point = nextPoints.remove(0);//若沒有訪問過//System.out.println(point.x);//System.out.println(point.y);if(!isVisited[point.x][point.y]){traversalChessboard(chessboard,point.x, point.y, step+1);}}//判斷馬兒是否完成了任務,使用 step和應該走的步數比較,//如果沒有達到數量,則表示沒有完成任務,將整個棋盤置0)//說明: step<X*Y 成立的情況有兩種// 1.棋盤到目前位置,仍然沒有走完// 2.棋盤處于一個回溯過程if(step<X*Y&&!isFinished){chessboard[row][col]=0;isVisited[row][col]=false;}else{isFinished=true;}}/*** *功能:根據當前位置(Point對象),計算馬兒還能走哪些位置(Point),* 并放入到一個集合中(ArrayList),最多有8個位置* @param curPoint 當前位置* @return*/public static ArrayList<Point>next(Point curPoint){ArrayList<Point> points = new ArrayList<>();Point p1 = new Point();//馬可以走5這個位置if((p1.x= curPoint.x-2)>=0&&(p1.y= curPoint.y-1)>=0)points.add(new Point(p1));//馬可以走6這個位置if((p1.x= curPoint.x-1)>=0&&(p1.y= curPoint.y-2)>=0)points.add(new Point(p1));//馬可以走7這個位置if((p1.x= curPoint.x+1)<Y&&(p1.y= curPoint.y-2)>=0)points.add(new Point(p1));//馬可以走0這個位置if((p1.x= curPoint.x+2)<Y&&(p1.y= curPoint.y-1)>=0)points.add(new Point(p1));//馬可以走1這個位置if((p1.x= curPoint.x+2)<Y&&(p1.y= curPoint.y+1)<X)points.add(new Point(p1));//馬可以走2這個位置if((p1.x= curPoint.x+1)<Y&&(p1.y= curPoint.y+2)<X)points.add(new Point(p1));//馬可以走3這個位置if((p1.x= curPoint.x-1)>=0&&(p1.y= curPoint.y+2)<X)points.add(new Point(p1));//馬可以走4這個位置if((p1.x= curPoint.x-2)>=0&&(p1.y= curPoint.y+1)<X)points.add(p1);return points;}/*** 將集合中的 Point 對象根據其下一步可移動選項的個數升序排序* @param ps*/public static void sort(ArrayList<Point>ps){ps.sort(new Comparator<Point>() {@Overridepublic int compare(Point o1, Point o2) {return next(o1).size() - next(o2).size();}});}}
總結
以上是生活随笔為你收集整理的【尚硅谷_数据结构与算法】十二、算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ghost 博客平台安装和配置
- 下一篇: 伽辽金法