KMP算法理解(更新)

KMP算法是一個非常經典的字符串匹配的算法,它講的是,我們給定兩個字符串str1與str2,長度分別問N和M,實現一個算法,如果字符串str1,包含str2,則返回str2在str1中的開始位置,不包含則返回-1。題意很容易理解,如下圖:當str1與str2爲abcdabce與abce的時候,二者匹配,返回abce在str1中開始位置的下標3,當str1與str2爲abcdabce與abcf的時候,二者不存在包含關係,返回-1。大多數人看到這個題目,第一個想到的方法肯定是暴力比對,將str2從str1的第一個字符位置開始比對,如果能夠匹配上則返回,不能匹配就從第二個,第三個…直到匹配上或者匹配失敗,不可否認,採用這種方法確實能夠完成需求,但是所耗費的時間複雜度是O(M*N),那麼有沒有什麼能夠降低時間複雜度的辦法呢,我們的KMP算法就閃亮登場了,在開始介紹之前,我們先了解一個概念,字符串str的nextArr數組,這個數組有什麼特點:1.這個數組的長度與str字符串的長度一樣2.nextArr[i]的含義就是str[i]之前的字符串str[0…i-1]中必須以str[i-1]結尾的後綴子串,(不能包含str[0])與必須以str[0]開頭的前綴子串(不能包含str[i-1])的最大匹配長度。下面舉個簡單的例子,大家就明白了,假設我們有一個字符串str爲abcdabcd,那麼它的nextArr數組是怎麼得出來的呢,先把結果列出來,結果就是:[-1,0,0,0,0,1,2,3]。下面就詳細介紹一下這個結果是怎麼出來的: 1. 首先當i=0的時候,str[0]之前沒有任何字符串了,默認此時值爲-1 2. 接下來i=2的時候,str[1]之前只有一個字符串a,此時默認爲0 3. 接下來i=2,str[2]之前的字符串爲ab,此時a與b不等,nextArr[2]=0 4. 接下來i=3,str[3]之前的字符串爲abc,nextArr[3]=0 5. 同理,nextArr[4]=0 6. 一直到i=5的時候,str[5]之前是abcda,此時有一個字符a,是前綴子串與後綴子串的匹配字符,nextArr[5]=1 7. 當i=6的時候,str[5]之前是abcdab,此時有一個字符串ab,是前綴子串與後綴子串的最長匹配,nextArr[6]=2 8. 同理nextArr[7]=3下面看當我們得到這個nextArr數組之後,它是如何優化時間複雜度的。回到我們最初的問題上來,我們要解決的問題是判斷字符串str1是否包含str2如上圖所示,a串與b串是字符串str2[0…j-1]的前綴子串與後綴子串相匹配的最長字符串,即a與b相等,假設str1與str2在叉號左邊部分完全匹配上了,但是到str2[j]的位置匹配失敗,即str2[j]不等於str1[i],此時,注意,我們的做法不再是將str2只向右滑動一個單位,而是向右滑動j-nextarr[j] (已匹配長度-前綴與後綴最大公共長度)個單位,然後再繼續以上過程,開始匹配,這樣就完成了時間複雜度的優化工作,這一步也是KMP算法的核心步驟。現在假設我們有一個方法可以獲取一個字符串的nextArr數組,假設這個方法名爲getNextArr,下面附上KMP算法的代碼: pubic int getIndex (String str1, String str2){
//如果兩個字符串都爲空,或者匹配串的長度大於被匹配串的長度,直接返回-1
if (str1 == null || str2 == null || str2.length > str1.length) {
return -1;
}
char[] ch1 = str1.toCharArray();
char[] ch2 = str2.toCharArray();
//i,j分別表示在str1與str2的指針,當j走到str2的最後一個字符的時候,說明匹配成功。
int i = 0;
int j = 0;
int[] nextArr = getNextArray(ch2);
while (i < ch1.length && j < ch2.length) {
//如果匹配上就進行下個位置的對比
if (ch1[i] == ch2[j]) {
i++;
j++;
}
//如果nextArr[j]==-1,則說明匹配串index爲0,只有此處默認值爲-1;
else if (nextArr[j] == -1) {
i++;
}
//否則,匹配串向右滑動,這裏的j = nextArr[j]可以理解爲向右滑動
else {
j = nextArr[j];
}
}
return j == ch2.length ? i - j : -1;

	}接下來就介紹一下如何獲取nextArr數組前面就介紹過按照規定字符串第一個字符對應的數組值爲-1,第二個爲0,即nextArr[0] = -1;nextArr[1] = 0;對於後面的求解過程,下面詳細介紹:因爲是從左到右依次求解,所以當求解nextArr[i]的時候,nextArr[i-1]已經求解出來,通過它的值可以知道B字符前字符串的最長前綴與最長後綴的匹配區域,a區域與b區域,字符C與字符B分別是緊貼着這兩個區域後面的字符,由此可知,如果C字符與B字符相同,那麼nextArr[i]=nextArr[i-1]+1。如果字符C與字符B不等,那麼就看字符C之前的前綴與後綴的匹配情況了,假設字符C是第cn個字符,那麼nextArr[cn]就是其最長前綴與後綴匹配的長度,如下圖所示,那麼,n與m兩個就是最長前綴與後綴區域,m'是b區域的最右區域且長度與m區域長度一致,那麼m與m'一定是相等的,字符D是n區域後面一個元素,如果D字符與B字符相等,那麼nextArr[i]=nextArr[cn]+1。如果不等那麼繼續往前跳到字符D,之後的過程與跳到C一致,每跳一次都會出現一個字符與B比較,如果相等,nextArr[i]就可以確定。如果跳到最左的位置,此時nextArr[0]=-1,此時說明字符A之前的字符串不存在前綴後綴匹配,令nextArr[i]=0;具體代碼如下:public int[] getNextArray (String s){
		char[] ch = s.toCharArray();
		if (ch.length == 1) {
			return new int[]{-1};
		}
		int[] nextArr = new int[ch.length];
		nextArr[0] = -1;
		nextArr[1] = 0;
		int pos = 2;
		int cn = 0;
		while (pos < next.length) {
		//如果字符B等於字符C,加一
			if (ch[pos - 1] == ch[cn]) {
				next[pos++] = ++cn;
			} else if (cn > 0) {
				cn = next[cn];
			} else {
				next[pos++] = 0;
			}
		}

		return next;

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