1、二分查找算法
- 之前有說過二分查找算法,是使用遞歸的方式,下面我們來寫一個二分查找算法的非遞歸方式
- 二分查找法只適用於從有序的數列中進行查找(比如數字和字母等),將數列排序後再進行查找
- 二分查找法的運行時間爲對數時間O(㏒₂n),即查找到需要的目標位置最多只需要㏒₂n步,假設從[0,99]的隊列(100個數,即n=100)中尋到目標數30,則需要查找步數爲㏒₂100 ,即最多需要查找7次( 2 6 < 100 < 27)。
二分查找算法(非遞歸)代碼實現:
public class BinarySearch {
public static void main(String[] args) {
//測試:
int[] arr= {1,3, 8, 10, 11, 67, 100};
int index=binarySearch(arr, 111);
System.out.println("index="+index);
}
//二分查找非遞歸實現
/**
*
* @param arr 待查找的數組,升序排列
* @param target 需查找的數
* @return 對應下標,-1表示沒有找到
*/
public static int binarySearch(int[] arr, int target) {
int left=0;
int right= arr.length-1;
while(left<=right) {
int mid = (left+right)/2;
if(arr[mid]==target) {
return mid;
}else if(arr[mid]>target) {
right=mid-1;//向左查找
}else {
left=mid+1;//向右查找
}
}
return -1;
}
}
2、分治算法(漢諾塔問題)
分治算法介紹:
- 分治法是一種很重要的算法。字面上的解釋是“分而治之”,就是把一個複雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。這個技巧是很多高效算法的基礎,如排序算法(快速排序,歸併排序),傅立葉變換(快速傅立葉變換)……
- 分治算法可以求解的一些經典問題:
(1) 二分搜索
(2) 大整數乘法
(3) 棋盤覆蓋
(4) 合併排序
(5) 快速排序
(6) 線性時間選擇
(7) 最接近點對問題
(8) 循環賽日程表
(9) 漢諾塔
分治法在每一層遞歸上都有三個步驟:
- 分解:將原問題分解爲若干個規模較小,相互獨立,與原問題形式相同的子問題
- 解決:若子問題規模較小而容易被解決則直接解,否則遞歸地解各個子問題
- 合併:將各個子問題的解合併爲原問題的解。
分治算法最佳實踐-漢諾塔問題:
- 如果是有一個盤, 則直接A->C
- 如果我們有 n >= 2 情況,我們總是可以看做是兩個盤 1.最下邊的盤 2. 上面的所有盤
(1) 先把最上面的盤 A->B
(2) 把最下邊的盤 A->C
(3) 把B塔的所有盤 從 B->C
代碼實現:
public class HanoiTower {
public static void main(String[] args) {
hanoiTower(3, 'A', 'B', 'C');
}
//漢諾塔移動方法
public static void hanoiTower(int num,char a,char b, char c) {
//如果只有一個盤
if(num==1) {
System.out.println("第1個盤"+a+"->"+c);
}else {
//如果我們有 n >= 2 情況,我們總是可以看做是兩個盤 1.最下邊的一個盤 2.上面的所有盤
//1.先把 上面的所有盤 A->B,移動過程中會使用C
hanoiTower(num-1, a, c, b);
//把最下邊的盤 A->C
System.out.println("第"+num+"個盤"+a+"->"+c);
//把B塔的所有盤 從 B->C,移動過程中會使用A
hanoiTower(num-1, b, a, c);
}
}
}
3、動態規劃算法
動態規劃算法介紹:
- 動態規劃(Dynamic Programming)算法的核心思想是:將大問題劃分爲小問題進行解決,從而一步步獲取最優解的處理算法。
- 動態規劃算法與分治算法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到原問題的解。
- 與分治法不同的是,適合於用動態規劃求解的問題,經分解得到子問題往往不是互相獨立的。 (即下一個子階段的求解是建立在上一個子階段的解的基礎上,進行進一步的求解 )
- 動態規劃可以通過填表的方式來逐步推進,得到最優解。
動態規劃算法最佳實踐—揹包問題:
揹包問題:有一個揹包,容量爲4磅 , 現有如下物品:
- 要求達到的目標爲裝入的揹包的總價值最大,並且重量不超出
- 要求裝入的物品不能重複
思路分析:
算法的主要思想,利用動態規劃來解決。每次遍歷到的第i個物品,根據w[i]和val[i]來確定是否需要將該物品放入揹包中。即對於給定的n個物品,設val[i]、w[i]分別爲第i個物品的價值和重量,C爲揹包的容量。再令v[i][j]表示在前i個物品中能夠裝入容量爲j的揹包中的最大價值。則我們有下面的結果:
- v[i][0]=v[0][j]=0;
表示填入表第一行和第一列是0 - 當w[i]> j 時:v[i][j]=v[i-1][j];
當準備加入新增的商品的容量大於當前揹包的容量時,就直接使用上一個單元格的裝入策略。 - 當j>=w[i]時: v[i][j]=max{v[i-1][j], val[i]+v[i-1][j-w[i]]};
當 準備加入的新增的商品的容量小於等於當前揹包的容量,裝入的方式:
v[i-1][j]: 就是上一個單元格的裝入的最大值
val[i]:表示當前商品的價值
v[i-1][j-w[i]]:裝入i-1商品,到剩餘空間j-w[i]的最大值
然後取最大的價值裝入即可
最後得到的結果爲:
最後一個(紅色字體)即爲最優的裝法。
代碼實現:
public class KnapsackProblem {
public static void main(String[] args) {
int[] w = { 1, 4, 3 };// 物品的重量
int[] val = { 1500, 3000, 2000 };// 物品的價值
int m = 4;// 揹包的容量
int n = val.length;// 物品的個數
// 創建二維數組
int[][] v = new int[n + 1][m + 1];// 表示在前i個物品中能夠裝入容量爲j的揹包中的最大價值
// 爲了記錄放入商品的情況,我們再創建一個二維數組
int[][] path = new int[n + 1][m + 1];
// 初始化第一行和第一列(也可以不處理,因爲默認就爲0)
for (int i = 0; i < n + 1; i++) {// 第一列設爲0
v[i][0] = 0;
}
for (int i = 0; i < m + 1; i++) {// 第一列設爲0
v[0][i] = 0;
}
// 根據公式進行動態規劃處理
for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
if (w[i - 1] > j) {// 因爲i是從1開始的表示第一個物品,但第一個物品對應的重量在w數組中對應數值的下標爲0,所以需要-1
v[i][j] = v[i - 1][j];
} else {// 同理,val和w所對應的都需要-1
// v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
// 爲了記錄,不能簡單地直接使用這個公式
// 需要使用if-else來處理
if (v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]) {
v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];// 即我們將第i個商品放入了揹包
path[i][j] = 1;
} else {
v[i][j] = v[i - 1][j];
}
}
}
}
// 輸出二維數組
for (int i = 0; i < n + 1; i++) {
for (int j = 0; j < m + 1; j++) {
System.out.print(v[i][j] + "\t");
}
System.out.println();
}
// 輸出最後放入的那些商品
int i = n;// 行的最大下標
int j = m;// 列的最大下標
while (i > 0 && j > 0) {// 從path的最後開始找
if (path[i][j] == 1) {
System.out.printf("第%d個商品放入到揹包\n", i);
j -= w[i - 1];
}
i--;
}
}
}
結果:
0 0 0 0 0
0 1500 1500 1500 1500
0 1500 1500 1500 3000
0 1500 1500 2000 3500
第3個商品放入到揹包
第1個商品放入到揹包
4、KMP查找算法
應用場景-字符串匹配問題:
暴力匹配算法:
- 如果當前字符匹配成功(即str1[i] == str2[j]),則i++,j++,繼續匹配下一個字符
- 如果失配(即str1[i]! = str2[j]),令i = i - (j - 1),j = 0。相當於每次匹配失敗時,i 回溯,j被置爲0。
- 用暴力方法解決的話就會有大量的回溯,每次只移動一位,若是不匹配,移動到下一位接着判斷,浪費了大量的時間。
- 暴力匹配算法實現:
public class ViolenceMatch {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
int index = violenceMatch(str1, str2);
System.out.println(index);
}
// 暴力匹配方法
public static int violenceMatch(String str1, String str2) {
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int s1Len = s1.length;
int s2Len = s2.length;
int i = 0;// 指向s1
int j = 0;// 指向s2
while (i < s1Len && j < s2Len) {
if (s1[i] == s2[j]) {
i++;
j++;
} else {
i = i - (j - 1);
j = 0;
}
}
if (j == s2Len) {
return i - j;
} else {
return -1;
}
}
}
KMP算法介紹:
- KMP是一個解決模式串在文本串是否出現過,如果出現過,最早出現的位置的經典算法。
- Knuth-Morris-Pratt 字符串查找算法,簡稱爲 “KMP算法”,常用於在一個文本串S內查找一個模式串P的出現位置,這個算法由Donald Knuth、Vaughan Pratt、James H.Morris三人於1977年聯合發表,故取這3人的姓氏命名此算法。
- KMP方法算法就利用之前判斷過信息,通過一個next數組,保存模式串中前後最長公共子序列的長度,每次回溯時,通過next數組找到,前面匹配過的位置,省去了大量的計算時間。
圖解:
代碼實現:
import java.util.Arrays;
public class KMPAlgorithm {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
int[] next = kmpNext(str2);
System.out.println(Arrays.toString(next));
int index = kmpSearch(str1, str2, next);
System.out.println("index = " + index);
}
// KMP算法
public static int kmpSearch(String str1, String str2, int[] next) {
// 遍歷
for (int i = 0, j = 0; i < str1.length(); i++) {
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
j = next[j - 1];
}
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
if (j == str2.length()) {// 找到了
return i - j + 1;
}
}
return -1;
}
// 獲取一個字符串的部分匹配表
public static int[] kmpNext(String dest) {
// 創建一個next數組保存部分匹配表
int[] next = new int[dest.length()];
next[0] = 0;// 如果字符串長度爲1,部分匹配值就爲0
for (int i = 1, j = 0; i < dest.length(); i++) {
while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j - 1];
}
if (dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
}
結果:
[0, 0, 0, 0, 1, 2, 0]
index = 15
具體的可以參考資料:KMP算法詳解
5、貪心算法
貪心算法介紹:
- 貪婪算法(貪心算法)是指在對問題進行求解時,在每一步選擇中都採取最好或者最優(即最有利)的選擇,從而希望能夠導致結果是最好或者最優的算法。
- 貪婪算法所得到的結果不一定是最優的結果(有時候會是最優解),但是都是相對近似(接近)最優解的結果。
貪心算法最佳應用—集合覆蓋:
假設存在如下表的需要付費的廣播臺,以及廣播臺信號可以覆蓋的地區。 如何選擇最少的廣播臺,讓所有的地區都可以接收到信號。
思路分析:
- 遍歷所有的廣播電臺, 找到一個覆蓋了最多未覆蓋的地區的電臺(此電臺可能包含一些已覆蓋的地區,但沒有關係)。
- 將這個電臺加入到一個集合中(比如ArrayList), 想辦法把該電臺覆蓋的地區在下次比較時去掉。
- 重複第1步直到覆蓋了全部的地區。
代碼實現:
package greedy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
public class GreedyAlgorithm {
public static void main(String[] args) {
// 創建廣播點臺
HashMap<String, HashSet<String>> broadCasts = new HashMap<String, HashSet<String>>();
// 將各個電臺放入到broadCasts中
HashSet<String> hashSet1 = new HashSet<String>();
hashSet1.add("北京");
hashSet1.add("上海");
hashSet1.add("天津");
HashSet<String> hashSet2 = new HashSet<String>();
hashSet2.add("廣州");
hashSet2.add("北京");
hashSet2.add("深圳");
HashSet<String> hashSet3 = new HashSet<String>();
hashSet3.add("成都");
hashSet3.add("上海");
hashSet3.add("杭州");
HashSet<String> hashSet4 = new HashSet<String>();
hashSet4.add("上海");
hashSet4.add("天津");
HashSet<String> hashSet5 = new HashSet<String>();
hashSet5.add("杭州");
hashSet5.add("大連");
// 加入到map
broadCasts.put("K1", hashSet1);
broadCasts.put("K2", hashSet2);
broadCasts.put("K3", hashSet3);
broadCasts.put("K4", hashSet4);
broadCasts.put("K5", hashSet5);
// allAreas存放所有地區
HashSet<String> allAreas = new HashSet<String>();
allAreas.add("北京");
allAreas.add("上海");
allAreas.add("天津");
allAreas.add("廣州");
allAreas.add("深圳");
allAreas.add("成都");
allAreas.add("杭州");
allAreas.add("大連");
// 創建一個ArrayList,存放選擇的電臺集合
ArrayList<String> selects = new ArrayList<String>();
// 定義一個臨時集合,在遍歷過程中,存放遍歷過程中電臺覆蓋的地區和當前還未覆蓋的地區的交集
HashSet<String> tempSet1 = new HashSet<String>();
HashSet<String> tempSet2 = new HashSet<String>();
// 定義一個maxKey,保存在一次遍歷中,能覆蓋最多未覆蓋地區所對應的電臺的key
String maxKey = null;
while (allAreas.size() != 0) {// 如果不爲0,則表示還未覆蓋到所有地區,則繼續選擇
// 每進行一次while需要把maxKey置空
maxKey = null;
// 遍歷broadCasts,取出對應的key
for (String key : broadCasts.keySet()) {
// 每進行一次for,需要將tempSet清空
tempSet1.clear();
tempSet2.clear();
// 當前的key所能覆蓋的地區
tempSet1.addAll(broadCasts.get(key));
// 存放目前來說擁有最大的覆蓋地區的電臺的覆蓋地區
if (maxKey != null) {
tempSet2.addAll(broadCasts.get(maxKey));
}
// 求出tempSet1和allAreas兩個集合的交集,並將結果賦給tempSet1集合
tempSet1.retainAll(allAreas);
// 求出tempSet2和allAreas兩個集合的交集,並將結果賦給tempSet2集合
tempSet2.retainAll(allAreas);
// 如果當前這個集合所包含的未覆蓋的地區的數量,比maxKey指向的集合地區還多,就需要重置maxKey
if (tempSet1.size() > 0 && (maxKey == null || tempSet1.size() > tempSet2.size())) {
// tempSet1.size() > tempSet2.size()體現了貪心算法的特點,每次都選擇最優的
maxKey = key;
}
}
// maxKey!=null,就應該將maxKey加入selects中
if (maxKey != null) {
selects.add(maxKey);
// 將maxKey指向的廣播電臺覆蓋的地區從allAreas中去掉
allAreas.removeAll(broadCasts.get(maxKey));
}
}
System.out.println("選擇的結果爲:" + selects);// K1,K2,K3,K5
}
}
結果:
選擇的結果爲:[K1, K2, K3, K5]
貪心算法注意事項和細節:
- 貪婪算法所得到的結果不一定是最優的結果(有時候會是最優解),但是都是相對近似(接近)最優解的結果
- 比如上題的算法選出的是K1,K2,K3,K5,符合覆蓋了全部的地區
- 但是我們發現 K2,K3,K4,K5也可以覆蓋全部地區,如果K2 的使用成本低於K1,那麼我們上題的 K1,K2,K3,K5雖然是滿足條件,但是並不是最優的。