Java十大算法(1):二分查找算法、分治算法、動態規劃算法、KMP查找算法、貪心算法

1、二分查找算法

  1. 之前有說過二分查找算法,是使用遞歸的方式,下面我們來寫一個二分查找算法的非遞歸方式
  2. 二分查找法只適用於從有序的數列中進行查找(比如數字和字母等),將數列排序後再進行查找
  3. 二分查找法的運行時間爲對數時間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. 分治算法可以求解的一些經典問題:
    (1) 二分搜索
    (2) 大整數乘法
    (3) 棋盤覆蓋
    (4) 合併排序
    (5) 快速排序
    (6) 線性時間選擇
    (7) 最接近點對問題
    (8) 循環賽日程表
    (9) 漢諾塔

分治法在每一層遞歸上都有三個步驟:

  1. 分解:將原問題分解爲若干個規模較小,相互獨立,與原問題形式相同的子問題
  2. 解決:若子問題規模較小而容易被解決則直接解,否則遞歸地解各個子問題
  3. 合併:將各個子問題的解合併爲原問題的解。

分治算法最佳實踐-漢諾塔問題:

  1. 如果是有一個盤, 則直接A->C
  2. 如果我們有 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、動態規劃算法

動態規劃算法介紹:

  1. 動態規劃(Dynamic Programming)算法的核心思想是:將大問題劃分爲小問題進行解決,從而一步步獲取最優解的處理算法。
  2. 動態規劃算法與分治算法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到原問題的解。
  3. 與分治法不同的是,適合於用動態規劃求解的問題,經分解得到子問題往往不是互相獨立的。 (即下一個子階段的求解是建立在上一個子階段的解的基礎上,進行進一步的求解 )
  4. 動態規劃可以通過填表的方式來逐步推進,得到最優解。

動態規劃算法最佳實踐—揹包問題:
揹包問題:有一個揹包,容量爲4磅 , 現有如下物品:
在這裏插入圖片描述

  1. 要求達到的目標爲裝入的揹包的總價值最大,並且重量不超出
  2. 要求裝入的物品不能重複

思路分析:
算法的主要思想,利用動態規劃來解決。每次遍歷到的第i個物品,根據w[i]和val[i]來確定是否需要將該物品放入揹包中。即對於給定的n個物品,設val[i]、w[i]分別爲第i個物品的價值和重量,C爲揹包的容量。再令v[i][j]表示在前i個物品中能夠裝入容量爲j的揹包中的最大價值。則我們有下面的結果:

  1. v[i][0]=v[0][j]=0;
    表示填入表第一行和第一列是0
  2. 當w[i]> j 時:v[i][j]=v[i-1][j];
    當準備加入新增的商品的容量大於當前揹包的容量時,就直接使用上一個單元格的裝入策略。
  3. 當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	35003個商品放入到揹包
第1個商品放入到揹包

4、KMP查找算法

應用場景-字符串匹配問題:

暴力匹配算法:

  1. 如果當前字符匹配成功(即str1[i] == str2[j]),則i++,j++,繼續匹配下一個字符
  2. 如果失配(即str1[i]! = str2[j]),令i = i - (j - 1),j = 0。相當於每次匹配失敗時,i 回溯,j被置爲0。
  3. 用暴力方法解決的話就會有大量的回溯,每次只移動一位,若是不匹配,移動到下一位接着判斷,浪費了大量的時間。
  4. 暴力匹配算法實現:
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算法介紹:

  1. KMP是一個解決模式串在文本串是否出現過,如果出現過,最早出現的位置的經典算法。
  2. Knuth-Morris-Pratt 字符串查找算法,簡稱爲 “KMP算法”,常用於在一個文本串S內查找一個模式串P的出現位置,這個算法由Donald Knuth、Vaughan Pratt、James H.Morris三人於1977年聯合發表,故取這3人的姓氏命名此算法。
  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、貪心算法

貪心算法介紹:

  1. 貪婪算法(貪心算法)是指在對問題進行求解時,在每一步選擇中都採取最好或者最優(即最有利)的選擇,從而希望能夠導致結果是最好或者最優的算法。
  2. 貪婪算法所得到的結果不一定是最優的結果(有時候會是最優解),但是都是相對近似(接近)最優解的結果。

貪心算法最佳應用—集合覆蓋:
假設存在如下表的需要付費的廣播臺,以及廣播臺信號可以覆蓋的地區。 如何選擇最少的廣播臺,讓所有的地區都可以接收到信號。
在這裏插入圖片描述
思路分析:

  1. 遍歷所有的廣播電臺, 找到一個覆蓋了最多未覆蓋的地區的電臺(此電臺可能包含一些已覆蓋的地區,但沒有關係)。
  2. 將這個電臺加入到一個集合中(比如ArrayList), 想辦法把該電臺覆蓋的地區在下次比較時去掉。
  3. 重複第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]

貪心算法注意事項和細節:

  1. 貪婪算法所得到的結果不一定是最優的結果(有時候會是最優解),但是都是相對近似(接近)最優解的結果
  2. 比如上題的算法選出的是K1,K2,K3,K5,符合覆蓋了全部的地區
  3. 但是我們發現 K2,K3,K4,K5也可以覆蓋全部地區,如果K2 的使用成本低於K1,那麼我們上題的 K1,K2,K3,K5雖然是滿足條件,但是並不是最優的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章