一、二分查詢演算法(非遞迴)
1,遞迴版二分查詢演算法
2,非遞迴二分查詢演算法介紹
原始碼:二分查詢(非遞迴)
-
二分查詢法只適用於從有序的數列中進行查詢(比如數字和字母等),將數列排序後再進行查詢
-
二分查詢法的執行時間為對數時間 O(㏒₂n) ,即查詢到需要的目標位置最多隻需要㏒₂n步
3,程式碼實現
public static int search(int[] arr, int val) { int left = 0; int right = arr.length - 1; while (left <= right) { int mid = (left + right) / 2; if (arr[mid] == val) { return mid; } else if (arr[mid] > val) { right = mid - 1; } else { left = mid + 1; } } return -1; }
二、分治演算法
原始碼:漢諾塔
1,分治演算法介紹
分治法是一種很重要的演算法。字面上的解釋是“分而治之”,就是把一個複雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。
分治演算法求解的經典問題:二分搜尋、大整數乘法、歸併排序、快排、漢諾塔等
2,分治演算法基本步驟
分治法在每一層遞迴上都有三個步驟:
- 分解:將原問題分解為若干個規模較小,相互獨立,與原問題形式相同的子問題
- 解決:若子問題規模較小而容易被解決則直接解,否則遞迴地解各個子問題
- 合併:將各個子問題的解合併為原問題的解。
3,漢諾塔
a)介紹
如下圖所示,從左到右有A、B、C三根柱子,其中A柱子上面有從小疊到大的n個圓盤,現要求將A柱子上的圓盤移到C柱子上去,期間只有一個原則:一次只能移到一個盤子且大盤子不能在小盤子上面,求移動的步驟和移動的次數
b)思路
- 如果只有一個盤時,A -> C
- 如果有n(大於1)個盤時
- 把n-1個盤 A -> B (藉助C)
- 把第n個盤 A -> C
- 把n-1個盤 B -> C (藉助A)
c)程式碼實現
/** * 移動盤子 * @param num 一共有多少個盤子 * @param a 開始的柱子 * @param b 輔助的柱子 * @param c 目標柱子 */ public static void hanoitower(int num, char a, char b, char c) { if (num == 1) { System.out.println("第1個盤為: " + a + " -> " + c); } else { hanoitower(num - 1, a, c, b); System.out.println("第" + num + "個盤為: " + a + " -> " + c); hanoitower(num - 1, b, a, c); } }
三、動態規劃
原始碼:揹包問題
1,介紹
-
動態規劃(Dynamic Programming)演算法的核心思想是:將大問題劃分為小問題進行解決,從而一步步獲取最優解的處理演算法
-
動態規劃演算法與分治演算法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到原問題的解。
-
與分治法不同的是,適合於用動態規劃求解的問題,經分解得到子問題往往不是互相獨立的。 ( 即下一個子階段的求解是建立在上一個子階段的解的基礎上,進行進一步的求解 )
-
動態規劃可以通過填表的方式來逐步推進,得到最優解
2,揹包問題
揹包問題主要是指一個給定容量的揹包、若干具有一定價值和重量的物品,如何選擇物品放入揹包使物品的價值最大。其中又分 01 揹包和完全揹包(完全揹包指的是:每種物品都有無限件可用)
3,案例
揹包問題:有一個揹包,容量為 4 磅 , 現有如下物品
- 要求達到的目標為裝入的揹包的總價值最大,並且重量不超出
-
要求裝入的物品不能重複
4,案例分析與求解
程式碼實現
/** * 求解01揹包問題 * * @param v 商品的價值 * @param w 商品的重量(體積) * @param c 商品的最大容量 */ public static void knapsackDim(int[] v, int[] w, int c) { //初始化二維陣列,行表示商品的體積w 列表示容量從0->c int size = w.length; int[][] dp = new int[size + 1][c + 1]; for (int i = 1; i <= size; i++) { for (int j = 0; j <= c; j++) { //當前商品的體積 大於 容量j 時 直接取上一行的資料 dp[i][j] = dp[i - 1][j]; if (w[i-1] <= j) { //①dp[i - 1][j - w[i - 1]]為上一行的當前可用體積-當前商品體積 得到減去當前商品重量之後的最大價值 + v[i-1] //②dp[i][j]實則為上一行的資料 與①直接比較大小 dp[i][j] = Math.max(dp[i][j], v[i - 1] + dp[i - 1][j - w[i - 1]]); } } } }
優化為一維陣列
/** * 揹包問題優化 使用一維陣列 * * @param v 商品的價值 * @param w 商品的重量(體積) * @param c 商品的最大容量 */ public static void knapsackSingle(int[] v, int[] w, int c) { int[] dp = new int[c + 1]; //第一次初始化dp for (int i = 0; i < c + 1; i++) { dp[i] = w[0] > i ? 0 : v[0]; } for (int i = 1; i < w.length; i++) { //防止前面資料被覆蓋,從後往前進行遍歷 for (int j = c; j >=0; j--) { if (w[i] <= j) { dp[j] = Math.max(dp[j], v[i] + dp[j - w[i]]); } } } }
四、KMP演算法
1,暴力匹配演算法
原始碼:暴力匹配
a)思路
如果用暴力匹配的思路,並假設現在 str1 匹配到 i 位置,子串 str2 匹配到 j 位置,則有: 1) 如果當前字元匹配成功(即 str1[i] == str2[j]),則 i++,j++,繼續匹配下一個字元 2) 如果失配(即 str1[i]! = str2[j]),令 i = i - (j - 1),j = 0。相當於每次匹配失敗時,i 回溯,j 被置為 0。 3) 用暴力方法解決的話就會有大量的回溯,每次只移動一位,若是不匹配,移動到下一位接著判斷,浪費了大量 的時間。
b)程式碼實現
/** * 暴力匹配 * @param str1 原始字串 * @param str2 匹配字串 */ public static int violenceMatch(String str1,String str2) { //表示字串str2的匹配的索引位置 int j; for (int i = 0; i < str1.length();) { j = 0; while (i < str1.length() && j < str2.length() && str1.charAt(i) == str2.charAt(j)) { i++; j++; } //將j匹配到最後一個字元 if (j==str2.length()) { return i-j; } i = i - j + 1; } return -1; }
2,KMP演算法
原始碼:KMP演算法
a)思路
-
尋找最長字首字尾“ABCDABD”
- 獲取next陣列
-
- 將next 陣列相當於“最大長度值” 整體向右移動一位,然後初始值賦為-1
- 若p[k] == p[j],則next[j + 1 ] = next [j] + 1 = k + 1;
- 若p[k ] ≠ p[j],如果此時p[ next[k] ] == p[j ],則next[ j + 1 ] = next[k] + 1,否則繼續遞迴字首索引k = next[k],而後重複此過程。
- 解釋為: 如下圖所示,假定給定模式串ABCDABCE,且已知next [j] = k(相當於“p0 pk-1” = “pj-k pj-1” = AB,可以看出k為2),現要求next [j + 1]等於多少?因為pk = pj = C,所以next[j + 1] = next[j] + 1 = k + 1(可以看出next[j + 1] = 3)。代表字元E前的模式串中,有長度k+1 的相同字首字尾。
- 但如果pk != pj 呢?說明“p0 pk-1 pk” ≠ “pj-k pj-1 pj”。換言之,當pk != pj後,字元E前有多大長度的相同字首字尾呢?很明顯,因為C不同於D,所以ABC 跟 ABD不相同,即字元E前的模式串沒有長度為k+1的相同字首字尾,也就不能再簡單的令:next[j + 1] = next[j] + 1 。所以,我們們只能去尋找長度更短一點的相同字首字尾。
-
/** * 求出一個字元陣列的next陣列 * * @param p 字元陣列 * @return next陣列 */ public static int[] getNextArray(char[] p) { int[] next = new int[p.length]; next[0] = -1; int k = -1; int j = 0; while (j < p.length - 1) { //p[k]表示字首 p[j]表示字尾 if (k == -1 || p[j] == p[k]) { // k++; // j++; next[++j] = ++k; } else { k = next[k]; } } return next; }
- 基於next陣列開始進行匹配
- P[0]跟S[0]匹配失敗。所以執行“如果j != -1,且當前字元匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]”,所以j = -1,故轉而執行“如果j = -1,或者當前字元匹配成功(即S[i] == P[j]),都令i++,j++”,得到i = 1,j = 0,即P[0]繼續跟S[1]匹配。
- P[0]跟S[1]又失配,j再次等於-1,i、j繼續自增,從而P[0]跟S[2]匹配。
- 直到P[0]跟S[4]匹配成功,開始執行此條指令的後半段:“如果j = -1,或者當前字元匹配成功(即S[i] == P[j]),都令i++,j++”。
- P[1]跟S[5]匹配成功,P[2]跟S[6]也匹配成功, ...,直到當匹配到P[6]處的字元D時失配(即S[10] != P[6]),由於P[6]處的D對應的next 值為2,所以下一步用P[2]處的字元C繼續跟S[10]匹配,相當於向右移動:j - next[j] = 6 - 2 =4 位。
- 向右移動4位後,P[2]處的C再次失配,由於C對應的next值為0,所以下一步用P[0]處的字元繼續跟S[10]匹配,相當於向右移動:j - next[j] = 2 - 0 = 2 位。
-
移動兩位之後,A 跟空格不匹配,模式串後移1 位。
- P[6]處的D再次失配,因為P[6]對應的next值為2,故下一步用P[2]字元C繼續跟文字串匹配,相當於模式串向右移動 j - next[j] = 6 - 2 = 4 位。
-
匹配成功,過程結束。
-
匹配過程一模一樣。也從側面佐證了,next 陣列確實是只要將各個最大字首字尾的公共元素的長度值右移一位,且把初值賦為-1 即可。
-
/** * 對主串s和模式串t進行KMP模式匹配 * * @param s 主串 * @param t 模式串 * @return 若匹配成功,返回t在s中的位置(第一個相同字元對應的位置),若匹配失敗,返回-1 */ public static int kmpMatch(String s, String t) { char[] s_arr = s.toCharArray(); char[] t_arr = t.toCharArray(); int[] next = getNextArray(t_arr); int i = 0, j = 0; while (i < s_arr.length && j < t_arr.length) { if (j == -1 || s_arr[i] == t_arr[j]) { i++; j++; } else j = next[j]; } if (j == t_arr.length) return i - j; else return -1; }
五、貪心演算法
1,應用場景
假設存在下面需要付費的廣播臺,以及廣播臺訊號可以覆蓋的地區。 如何選擇最少的廣播臺,讓所有的地區都可以接收到訊號。
2,貪心演算法介紹
-
貪婪演算法(貪心演算法)是指在對問題進行求解時,在每一步選擇中都採取最好或者最優(即最有利)的選擇,從而希望能夠導致結果是最好或者最優的演算法
- 貪婪演算法所得到的結果不一定是最優的結果(有時候會是最優解),但是都是相對近似(接近)最優解的結果
3,問題求解
a)思路分析
- 使用窮舉法實現,列出每個可能的廣播臺的集合,這被稱為冪集。假設總的有 n 個廣播臺,則廣播臺的組合總共有2ⁿ -1
-
使用貪婪演算法,效率高
- 遍歷所有的廣播電臺,找到一個覆蓋了最多未覆蓋的地區的電臺(採用retainAll方法,將當前集合與選擇集合的交集賦值給當前集合)
- 將這個電臺加入到集合中,去除該電臺覆蓋的地區
- 重複以上,直至覆蓋所有的地區
b)程式碼實現
public static void main(String[] args) { Map<String, Set<String>> map = new HashMap<>(); Set<String> set1 = new HashSet<>(); set1.add("北京"); set1.add("上海"); set1.add("天津"); Set<String> set2 = new HashSet<>(); set2.add("廣州"); set2.add("北京"); set2.add("深圳"); Set<String> set3 = new HashSet<>(); set3.add("成都"); set3.add("上海"); set3.add("杭州"); Set<String> set4 = new HashSet<>(); set4.add("上海"); set4.add("天津"); Set<String> set5 = new HashSet<>(); set5.add("杭州"); set5.add("大連"); map.put("K1", set1); map.put("K2", set2); map.put("K3", set3); map.put("K4", set4); map.put("K5", set5); Set<String> allAreas = new HashSet<>(); allAreas.addAll(set1); allAreas.addAll(set2); allAreas.addAll(set3); allAreas.addAll(set4); allAreas.addAll(set5); //儲存選擇的key List<String> selects = new ArrayList<>(); //定義此時最大的key String maxKey; //臨時儲存的set集合 Set<String> tempSet = new HashSet<>(); //如果allArea不為空則一直刪除 while (allAreas.size() != 0) { //清空臨時set tempSet.clear(); // maxSize = 0; maxKey = null; for (Map.Entry<String, Set<String>> entry : map.entrySet()) { tempSet = entry.getValue(); tempSet.retainAll(allAreas); if (tempSet.size() > 0 && (maxKey == null || tempSet.size() > map.get(maxKey).size())) { maxKey = entry.getKey(); } } if (maxKey != null) { tempSet = map.get(maxKey); selects.add(maxKey); allAreas.removeAll(tempSet); //此時可以將對應的key去除,這樣能在遍歷map的時候提高效率 map.remove(maxKey); } } System.out.println(selects); }