KMP算法

一、字符串匹配場景

KMP算法可以解決以字符串匹配爲模型的問題,算法應用場景非常廣泛,並不僅僅限於文本的匹配。

以簡單的字符串匹配爲例,現有兩個鏈分別爲source和target,

要在Source鏈中匹配Target鏈,很容易觀察出出從source鏈下標10的位置可以成功匹配,如下圖所示:

 

二、非KMP算法對此類問題的求解方式

在字符串匹配問題中,最直觀的想法就是,Source鏈保持不動,Target鏈從開始位置每次向後滑動一個字符長度,在該位置上按照字符對齊的位置逐個與Source鏈上的字符進行比較,若Target鏈上的所有字符在該位置均可以與Source鏈上對齊位置的字符成功匹配,則匹配結束,若失敗,則Target鏈向後滑動一個字符長度,繼續按照字符對齊位置逐比較,用一張圖來表示該過程:

按照此方法,直到在Source下標爲10處完成匹配,這養的複雜度是O(m*n)。

現在分析第n步和第n+1步,在第n步中,已經匹配了5個字符:ababa,但是因爲c與d不相等,匹配失敗,第n+1步Target鏈後移一個單位,從頭開始匹配。

在匹配過程中,Target字符串的前5個字符已經被遍歷併成功匹配,我們期望這5個字符串中有一些可以用於優化下一次匹配的信息,來看另一種第n+1步的做法:

按照最開始的思路繼續運行,在多進行幾步後,也可以到達上圖中的狀態,而且中間一定不存在成功匹配的情況,也就是說,如果按照最開始的方法,上圖中的匹配場景一定會到達,從第n步到優化後的第n+1步,既省略了好幾趟沒有成功的比較,又可以不從Target下標爲0的位置開始。優化過程利用了Target鏈本身的一些性質,KMP算法就是充分利用這些特性,下面就來分析這些特性!

三、KMP算法原理

首先說優化的目標:減少比較的次數,那麼該怎麼減少呢?先來個直觀一些的:顏色一樣的是相同的,第n步在箭頭指向的位置匹配失敗,下一步進行第n+1步:

這個圖並不能很好的表示,原因有兩個:第一,匹配過程中source鏈本身是不可知的,應該完全依靠Target鏈的信息;第二,能匹配的部分一定是相等的,在source鏈上再畫出是多餘的,改完之後(強調:第n步箭頭位置標示匹配失敗的地方,第n+1步箭頭位置標示待匹配的地方):

如果這兩張圖能看明白,那KMP的主要思想應該就懂了,關注點就在Target鏈上這種綠色的結構。

3.1字符串的特性分析

我們都知道,在匹配過程中,如果某次節點比較發現不相等,那麼是一定需要回退的,只是回退多少的問題,但優化後可以減少比較的次數,像上面優化的第n+1步,相比於原來的比較方式,就可以很大程度上減少比較次數。

在進行分析前,我們可以先思考一下,什麼情況下,在某個元素匹配失敗後,不用全部回退,很容易想到,那就是在target串中有重複的子串,假如他們從左到右分別叫爲substr1,substr2,並且substr1和substr2完全相等。

那麼當我們開始考慮回退多少時,有一件事一定已經發生,那就是substr2後面緊接着的一個節點匹配失敗了(這裏的subStr2可以爲空串,此處並不失一般性),這時候substr2已經完成了匹配,那麼substr2的位置一定可以成功匹配substr1,那在接下來的比較中,不就可以直接跳過這一部分的比較了?但這裏有一個條件,substr1必須是從下標爲0的位置開始的,不難想象這個條件,如果不從0開始,那在substr1之前的節點是無法保證一定能匹配到substr2前面緊挨着的節點上的。

在直觀上了解原理後,就要想辦法把這些特性表示出來,然後用到代碼裏,首先,要做一個定義:

對於任意一個字符串,存在相等最長前綴最長後綴,說白了就是在一個字符串中,從下標爲0的位置開始的一個子串(不包含最後一個字符)和從後面某位置開始的一個子串(一定要包含最後一個字符)相等,那麼這兩個子串分別就是相等最長前綴和最長後綴,下面列舉一些例子,左邊是字符串,右邊是最長前綴和最長後綴:

只有一個字母的時候就是空串“”,在此基礎上,爲了實現我們的算法,需要的就是根據當前剛剛匹配失敗的位置,想辦法得到一個能夠控制回退的值,也就是說已經成功匹配的那部分字符串中,包含着最優的回退方法,在KMP中,用一個整數來描述最優的回退的值,習慣把這些值存在名爲next的數組中,針對上面的示例,得到next數組存儲結果是:

這裏-1表示不存在,0表示存在長度爲1,2表示存在長度爲3,這是爲了和代碼相對應(好多程序員寫的書都是從第0章開始。。),也就是說next數組長度和Target數組長度一致,next[i]就表示Target[0:i]子串的最長前綴的長度值減一,這裏next[i]的值一定小於i。

再回頭看下我們之前匹配失敗的第n步,假設i,j分別爲當前比較的節點的下標,即i=9,j=5。

在這一步中,Target[5]匹配失敗了,但是我們知道,Target[0~2]與Target[2~4]是相等的,此時Target[2~4]與Source[6~8]是成功匹配的,所以Target[0~2]也一定可以和Source[6~8]成功匹配,而無需再做多餘的匹配,那麼下一步i,j的值就可以根據這個進行相應的變換,i=9,j=3,有沒有發現什麼規律?在成功匹配的那部分子串ababa中,next值爲2,j=2+1,到這應該就明白了,j=0,j=1,j=2那部分就是我們說的不用匹配的位置,直接從下一個位置開始就行了,所以j=2+1!。

 

3.2代碼該如何實現呢?

如果到這裏已經明白了大概的原理,那麼接下來就可以看看別人的代碼了>>_<<(別人的代碼,還是各種騷操作,自己能能實現,但就顯得有點複雜了,所以直接看優秀的代碼吧。。。)

通過之前分析知道,KMP最重要的還是要依據待匹配字符串,也就是Target的特性,得到next數組,所以先從得到next數組開始,那next數組該怎麼求呢,這裏就有一個比較有意思的事情,求next數組本身又是一個字符串匹配的過程,所以在求next的過程中,可以用到已經求得的next值。

假設我們現在求next[i+1],那麼我麼總希望結果是好的,那麼什麼算好的呢?在Target[0~i]中,存在相等最長前綴subStr1和最長後綴subStr2,兩個長度都是m,最好的情況就是Target[m]=Target[i+1],那next[i+1] = next[i] +1,舉個例子:

在abcabc中求next[5],那麼此時已經得到的subStr1= subStr2 = ab,m=2,next[4] = 1,發現Target[2]與Target[5]相等,那麼next[5]=next[4] + 1=2。

那如果情況不好怎麼辦?當發現Target[m]!=Target[i+1]時,我們可能還想繼續偷懶:那把subStr2縮短一點吧,留下後面的部分,subStr1也變短點,留下前面那部分,但是保證變短後的兩部分還是相等的,在進行同樣的比較,不就又可以少比較幾次了嗎?那怎麼實現呢,這部分也很有意思,我們來舉個例子:

對於abacabad,假如我們要求next[7],那麼在Target[0:6]中,此時已經得到的subStr1= subStr2 = aba,m=3,next[6] = 2,發現Target[m]!=Target[7],怎麼縮短呢,subStr1自己有最長前綴和最長後綴,subStr2也有自己的最長前綴和最長後綴,那麼我要開始說一句真理了:subStr1的最長前綴一定等於subStr2的最長後綴!爲什麼呢?因爲subStr1等於subStr2,最長前綴等於最長後綴,這是一定成立的,放到例子中,subStr1的前綴是a,subStr2的後綴也是a,則m=1,此時比較Target[m]==Target[7]就可以了!接下來就來看下求next的代碼:

public static int[] getNext(String target){
		int[] next = new int[target.length()];
		next[0] = -1; //一個字母的時候,不存在相等最長前綴和最長後綴,所以值爲0;
		int k = -1;//若下一個待求位置是next[i+1],則k的初始值爲next[i],因爲next[0]是固定的,下一個待求位置是next[1],所以k=next[0]=-1
		/**
		 * 這裏更符合意義的寫法是:
		 * int i = 0;
		 * next[i] = -1;
		 * int k = next[i];
		 * 直接給k=-1,如果不太理解過程就很難明白到底在幹嘛,還有就是需要注意,k+1就等於我們上面談到的m
		 */
		
		for(int i = 1; i < target.length(); i++){
			while(k > -1 && target.charAt(k + 1) !=  target.charAt(i)){ 
				//能運行到這裏,就說明不是我們最希望的狀況,而這個循環就是當狀況不好時,退而求其次,“縮短”能偷得懶。
				//k > -1有兩個作用,1是是防止訪問越界2是k如果<=-1表示不存在最長前/後綴,就沒有必要找了
				k = next[k];
			}
			//跳出循環有兩種情況,一種是找到了一個縮短後能用的,一個就是k等於-1了
		if (target.charAt(k + 1) ==  target.charAt(i)){
	            k = k + 1;
	        }
			next[i] = k;
		}
		
		return next;
	}

 

有了next數組,那下面就好說了,因爲在求next的過程中,我們一直在用KMP算法求next的值,在這裏代碼一般有兩種具體的實現方式,一種是先得到next數組,在匹配,一個是一遍匹配一遍生成next數組。我們這裏給出第一種實現。

 

public static int KMP(String target, String source){
		int next[] = getNext(target);
		int k = next[0];
		for(int i = 0; i < source.length(); i++){
			while(k > -1 && target.charAt(k + 1) != source.charAt(i)){
				k = next[k];
			}
			if(target.charAt(k + 1) == source.charAt(i)){
				k = k + 1;//成功匹配一個節點
			}
			if(k == target.length()-1){//上面一直說k等於已經匹配的長度-1
				return i - target.length() + 1;
			}
			
		}
		return -1;
	}

第一篇到此結束,參考博文:https://blog.csdn.net/starstar1992/article/details/54913261,如有疑問,歡迎大家交流分享。

 

 

 

 

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