左神算法課基礎班知識點總結(二)

KMP算法

KMP算法解決–字符串匹配問題
時間複雜度爲:O(N)
求字符串str1是否包含str2,若包含,則返回str2在str1中的起始位置;若不包含,則返回-1。
須知:
最大前綴和最大後綴:前綴不包含最後一個字符,後綴不包含第一個字符。
eg: 對於字符串 abcabcd,此時我們要求d位置的最大前綴和最大後綴,則
長度爲1時,前綴爲 a,後綴爲c;
長度爲2時,前綴爲ab,後綴爲bc;
長度爲3時,前綴爲abc,後綴爲abc,匹配;
長度爲4時,前綴爲abca,後綴爲cabc;
長度爲5時,前綴爲abcab,後綴爲bcabc;
由此得出,字符串d位置的最大前綴和最大後綴長度爲3;
next數組,是指字符串中各個字符對應的最大前綴和最大後綴長度組成的數組。
人爲規定:next[0] = -1; next[1] = 0; 且next數組的長度應和用來求next數組的字符串的長度相同。
至於普遍位置的next數組值如何求?在Code中體現。
Code:

public class Code_KMP{
	public static int getIndexOf(String s,String m){//母串 s,子串 m
		if(s == null || m == null || m.length() < 0 || s.length() < m.length()){
			return -1;
		}
		char[] str1 = s.toCharArray();
		char[] str2 = m.toCharArray();
		int i1 = 0;//用來表示str1的索引
		int i2 = 0;//用來表示str2的索引
		//首先,獲取str2的next數組
		int[] next = getNextArray(str2);
		//在獲取到str2的next數組的前提下,進行字符串匹配。
		while(i1 < str1.length && i2 < str2.length){
			if(str[i1] == str2[i2]){//當i1位置的元素和i2位置的元素相等時,二者同時向後移。
				i1++;
				i2++;
			}else if(next[i2] == -1){//當i1位置上的元素與i2位置上的元素不匹配時,同時i2位置又是子串的初始字符,則此時i1向後移,繼續與子串的初始字符進行匹配
				i1++;
			}else{////當i1位置上的元素與i2位置上的元素不匹配時,同時i2位置爲子串的一個普遍位置時,表示在二者的比較過程中,有一部分匹配,但是到了某個位置出現不匹配的情況;
				/*
				 * eg:
				 * 		str1:  a b c a b c f...
				 * 		str2:  a b c a b c g...
				 * 		當匹配到f!=g時,g的最大前綴爲abc,爲3即next[g] = 3,則此時str2向後移動至,g的最大後綴處,即str2的3位置,
				 * 	繼續進行匹配,則此時匹配變爲:
				 * 			         a b c a b c g...  i2位置就變爲了 next[g] = 3,處,繼續進行 a 和 f的匹配了;
				 *  
				 * 
				 */
				i2 = next[i2];	
			}
		}
		//最後,若匹配,則當i2==str2.length時,表示全部匹配,同時跳出循環;
		return i2 == str2.length ? i1 - i2 : -1;	
	}
	public static int[] getNextArray(char[] ms){
		if(ms.length == 1){
			return new int[]{-1};
		}
		//某個字符串的next數組長度一定和該字符串的長度一致。
		int[] next = new int[ms.length];
		next[0] = -1;
		next[1] = 0;
		int i = 2;
		int cn = 0;//用來標識每次跳的位置。
		while(i<next.length){
			if(ms[i-1] == ms[cn]){//cn == next[i-1];初始值
				/*
					i-1表示i位置的前一個元素,cn表示next[i-1];ms[cn]表示(i-1)位置的最大前綴的下一個元素.如果,i-1位置的元素與ms[cn]相等,則i位置的最大前綴/後綴長度值爲next[i-1] + 1;
				*/
				next[i++] = ++cn;//++cn的同時爲求(i+1)位置的next數組值做好準備;
			}else if(cn > 0){//若沒有匹配上,則cn向前跳(未越界的情況下),繼續尋找
				cn = next[cn];
			}else{
				next[i++] = 0;//當cn跳過界時,表示沒有匹配,此時i位置的next數組值爲0;
			}
		}
	}
}

KMP算法擴展題目一

題目:給定一個字符串str1,只能往str1後面添加字符變成str2;
要求:
str2必須包含兩個str1,兩個str1可以有重合,但是不能以同一個位置開頭。且str2儘量短。
最終返回str2
eg: str1 = 123,則str2 = 123123; str1 = 123123,則str2 = 123123123;
實質爲Next數組的應用
Code:

public class Code_KMP_ShortestHaveTwice{
	public static String answer(String str){
		if(str == null || str.length() == 0){
			return "";
		}
		char[] s = str.toCharArray();
		if(s.length == 1){
			return str + str;
		}
		if(s.length == 2){
			return s[0] == s[1] ? (str + String.valueOf(s[0])):(str + str);
		}
		//通過求字符串的最後一個字符的下一位置的next數組值(最大前綴/最大後綴),之後在原字符的基礎之上添加截取的字符。
		//使得結果字符串的長度最小。
		int endNext = endNextLength(s);
		return str + str.substring(endNext);//Java中的所有截取都是左閉右開。
	}
	public static int endNextLength(char[] s){
		int[] next = new int[s.length + 1];
		next[0] = -1;
		next[1] = 0;
		int i = 2;
		int cn = 0;
		while(i < next.length){
			if(s[i-1] == s[cn]){
				next[i++]=++cn;
			}else if(cn > 0){
				cn = next[cn];
			}else{
				next[i++] = 0;
			}
		}
		return next[next.length - 1];
	}
}

KMP算法擴展題目二

給定兩個二叉樹T1和T2,返回T1的某個子樹結構是否與T2的結構相等。
通過KMP如何實現?答:將二叉樹序列化,之後進行字符串的匹配。
Code:

public class Code_KMP_T1SubTreeEqualsT2{
	//二叉樹的數據結構
	public static class Node{
		public int value;
		public Node left;
		public Node right;
		public Node(int value){
			this.value = value;
		}
	}
	public static boolean isSubTree(Node t1,Node t2){
		//將二叉樹序列化,
		String str1 = serialByPre(t1);//先序遍歷序列化樹T1
		String str2 = serialByPre(t2);//先序遍歷序列化樹T2
		return getIndexOf(str1,str2) != -1;
	}
	
	public static String serialByPre(Node head){
		if(head == null){
			return "#!";
		}
		String res = head.value + "!";
		res += serialByPre(head.left);
		res += serialByPre(head.right);
		return res;
	}
	public static int getIndexOf(String s,String m){
		if(s == null || m == null || s.length() < m.length() || m.length() < 1){
			return -1;
		}
		char[] str1 = s.toCharArray();
		char[] str2 = m.toCharArray();
		//獲取str2的Next數組
		int[] next = getNextArray(str2);
		int i1 = 0;
		int i2 = 0;
		while(i1 < str1.length && i2 < str2.length){
			if(str1[i1] == str2[i2]){
				i1++;
				i2++;
			}else if(next[i2] == -1){
				i1++;
			}else{
				i2 = next[i2];
			}
		}
		return i2 == str2.length ? i1 - i2 : -1;
	}
	public static int[] getNextArray(char[] ms){
		if(ms.length == 1){
			return new int[]{-1};
		}
		int[] next = new int[ms.length];
		next[0] = -1;
		next[1] = 0;
		int i = 2;
		int cn = 0;
		while(i < next.length){
			if(ms[i-1] == ms[cn]){
				next[i++] = ++cn;
			}else if(cn > 0){
				cn = next[cn];
			}else{
				next[i++] = 0;
			}
		}
		return next;
	}
}

Manacher算法–馬拉車

要解決的問題:在一個字符串中,找到最長的迴文子串
eg: abc12321de --最長迴文子串爲 12321
以每個位置爲起始,向兩邊擴。
如何解決?–添加特殊字符,作爲軸,特殊字符沒有什麼要求,可以和字符串中的字符相同。
eg: a12321b —>#a#1#2#3#2#1#b# 這樣奇迴文和偶迴文都可以找到對應的"軸";
須知:
①從每個位置出發,擴出來的範圍稱爲迴文半徑/範圍
②最右迴文半徑/邊界 --R
③當我首次取得最右迴文邊界/半徑時的迴文中心–C;若R更新,則C也更新。若R不變,則C也不變。
每個位置擴出來的迴文半徑/範圍,都記錄在一個數組arr裏。
流程:
I:從左往右擴,如果一個位置它在最右迴文邊界以外,沒有蛋扯,直接暴力擴。
II:如果一個字符i在最右迴文邊界內部,i關於C做對稱點j,R關於C做對稱點L,
情況一:j的迴文半徑/範圍,全部在L到R的範圍內,這個時候i不用擴。
情況二:j的迴文半徑/範圍,有一部分在L到R的範圍以外,此時i也不用擴,且迴文半徑爲R-i;
情況三:j的迴文半徑/範圍,壓線(與邊界重合),此時需要嘗試是否能擴。

時間複雜度爲O(N)
Code:

public class Code_Manacher{
	public static int maxLcpsLength(String str){
		if(str == null || str.length() == 0{
			return 0;
		}
		char[] charArr  = manacherString(str);
		//用來記錄每個位置的最大回文半徑。
		int[] pArr = new int[charArr.length];
		//C用來代表迴文中心 (最早衝到R處的i)
		int C = -1;
		//R 用來代表最右迴文邊界
		int R = -1;
		int max = Integer.MIN_VALUE;
		for(int i=0; i != charArr.length; i++){
		/**
				 * 解釋:
				 	2 * C - i 表示的是 i 關於 C 的對稱點i1
				 	pArr[2 * C - i]表示的是 i1的最大回文長度。
				 	R - i 表示的是 i 到 R的長度;
				 	這句代碼的意思就是:
				 			當 i < R時,分以下三種情況:
				 							①i1的迴文邊界都在C到R的邊界以內。
				 							②i1的迴文邊界的左邊界小於C,也就是部分超出C,i的最大回文半徑小於R。
				 							③i以及i1的迴文邊界剛好與C和R的邊界重合。(壓線)
				 	而此處的pArr[i]獲取的是i位置最起碼的迴文長度。(無論哪種情況,都表示的是不用驗的區域。)
				 */
			pArr[i] = R > i ? Math.min(pArr[2 * C - i],R - i):1;
			while(i + pArr[i] < charArr.length && i - pArr[i] > -1){
				//看看是否能擴?
				if(charArr[i + pArr[i]]  == charArr[i-pArr[i]]){
					pArr[i]++;
				}else{
					break;
				}
			}
			//在i位置擴充之後,如果迴文邊界擴大了,則更新R及對應的C。
			if(i + pArr[i] > R){		
				R = i + pArr[i];
				C = i;
			}
			max = Math.max(max,pArr[i]);
		}
		return max - 1;
	}
	/**
		 * 這個方法就是--傳入一個普通的字符串,然後轉換成一個增加了輔助字符的數組;
		 * Str = "abcde";  ---> Str="#a#b#c#d#e#";
		 * 
	*/
	public static char[] manacherString(String str){
		char[] charArr = str.toCharArray();
		char[] res = new char[str.length * 2 + 1];
		int index = 0;
		for(int i = 0; i!=res.length; i++){
			//奇數 與 1做按位與運算,結果必爲1
			res[i] = i & 1 == 0 ? '#' : str[index++];
		}
		return res;
	}
}

Manacher算法擴展題目一

題目:給定一個字符串str1,只能往str1後面添加字符變成str2,要求str2整體都是迴文串且最短。
eg: str1 = abc12321,返回 str2 = abc12321cba
如何實現?
manacher在逐步擴的時候,當第一次R衝到str字符串的末尾時,停;
此時,最早的C我有了,R與L也可以確定,則將L之前的字符逆序添加到R之後即可。
Code:

public  class Code_Manacher_ShortestEnd{
	public static String shortestEnd(String str){
		if(str == null || str.length() == 0){
			return null;
		}
		char[] charArr = manacherString(str);
		int[] pArr = new int[charArr.length];
		int C = -1;
		inT R = -1;
		int maxContainsEnd = -1;
		for(int i = 0; i != charArr.length; i++){
			pArr[i] = R > i ? Math.min(pArr[2 * C - i],R - i) : 1;
			while(i + pArr[i] < charArr.length && i - pArr[i] > -1){
				if(char[i + pArr[i]] == char[i - pArr[i]]){
					pArr[i]++;
				}else{
					break;
				}
			}
			if(i + pArr[i] > R){
				R = i + pArr[i[;
				C = i;
			}
			if(R == charArr.length){
				maxContainsEnd = pArr[i];
				break;
			}
		}
		char[] res = new char[str.length() - maxContainsEnd + 1];
		for(int i = 0; i<res.length; i++){
			res[res.length - 1 - i] = charArr[i * 2 + 1];
		}
		return String.valueOf(res)
	}
	public static char[] manacherStrng(String str){
		char[] charArr = str.toCharArray();
		char[] res = new char[2 * charArr.length + 1];
		int index = 0;
		for(int i = 0; i<res.length; i++){
			res[i] = i & 1 == 0 ? '#' : charArr[index++];
		}
		return res;
	}
}

BFPRT算法

在一個無序數組中,求第K小/大的數。
** 時間複雜度爲O(N);**
思路:
①劃分值的選擇(核心)
第一步:將數組中元素每5個劃分爲一組,最後不足5個元素也稱爲一組。 O(1)
第二步:將劃分的組,組內排序,組間不排序。 O(1) * (N/5) = O(N)
第三步:獲取排好序的數組的中位數/上中位數,組成一個新的數組。–中位數數組。之後,自己調用自己,求得這個數組的中位數/上中位數。
第四步:將這個中位數拿去做劃分值。
②將劃分值拿去做partition過程。

之所以要這樣選擇劃分值,是因爲這樣可以確定的淘汰掉(3N/10)。
爲什麼?
eg:
1 5 6 8 9 長度爲N/5
此時中位數爲 6,就有(N/10)的數小於等於6,而每一個小於等於6的數,在各自的數組中又大於等於2個數,因此在原數組中就至少有(3N/10)小於等於6,最多有(7N/10)的數比6大。遞歸規模在估計的時候,應該按照最差的來估計,所以無論是最多有(7N/10)的數比我大,還是最少有(7N/10)的數比我小,都淘汰了(3N/10)的數.

Code:

	public class Code_BFPRT{

		public static int getMinKthByBFPRT(int[] arr,int K){
			int[] copyArr = copyArray(arr);
			return select(copyArr,0,copyArr.length - 1,K - 1);
		}

		public static int[] copyArray(int[] arr) {
			int[] res = new int[arr.length];
			for (int i = 0; i != res.length; i++) {
				res[i] = arr[i];
			}
			return res;
		}
		//在數組中給定一個範圍begin至end,在這個範圍上位於第i位置的數是哪個數?
		public static int select(int[] arr,int begin,int end,int i){	
			if(begin == end){
				return arr[begin];
			}
			//首先根據相應的流程獲取劃分值。
			int pivot = medianOfMedians(arr,begin,end);
			//之後,劃分值拿去partition,partition過程返回的是等於區的左右邊界。
			int[] pivotRange = partition(arr,begin,end,pivot);

			//若命中,直接返回;若未命中,或者走左邊,或者走右邊。
			if(i >= pivotRange[0] && i <= pivotRange[1]){
				return arr[i];
			}else if(i < pivotRange[0]){
				return select(arr,begin,pivotRange[0] - 1,i);
			}else{
				return select(arr,pivotRange[1] + 1, end,i);
			}
		}
		//獲取劃分值
		public static int medianOfMedians(int[] arr,int begin,int end){
			int num = end - begin + 1;
			int offset = num % 5  == 0 ? 0 : 1;
			//用來存放中位數的中位數數組
			int[] mArr = new int[num / 5 + offset];
			for(int i = 0; i < mArr.length; i++){
				int beginI = begin + i * 5;
				int endI = beginI +  4;
				mArrp[i] = getMedian(arr,beginI,Math.min(end,endI));
			}
			//mArr 遞歸調用select獲取中位數/上中位數
			return select(mArr,0,mArr.length-1,mArr.length / 2);
		}

		public static int[] partition(int[] arr,int begin,int end,int pivotValue){
			int small = begin - 1;
			int big = end + 1;
			int cur = begin;
			while(cur != big){
				if(arr[cur] < pivotValue){
					swap(arr,++small,cur++);
				}else if(arr[cur] > pivotValue){
					swap(arr,cur,--big);
				}else{
					cur++;
				}
			}
			int[] range = new int[2];
			range[0] = small + 1;
			range[1] = big - 1;
			return range;
		}

		public static int getMedian(int[] arr, int begin, int end) {
			insertionSort(arr, begin, end);
			int sum = end + begin;
			int mid = (sum / 2) + (sum % 2);
			return arr[mid];
		}

		public static void insertionSort(int[] arr, int begin, int end) {
			for (int i = begin + 1; i != end + 1; i++) {
				for (int j = i; j != begin; j--) {
					if (arr[j - 1] > arr[j]) {
						swap(arr, j - 1, j);
					} else {
						break;
					}
				}
			}
		}
	}

目前就更新到這裏,若覺得文章存在問題,歡迎斧正~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章