【算法基礎】KMP算法解析與實現

一,前言

字符串的模式匹配,即找尋字符串p第一次出現在字符t中的起始位置。計算機科學研究最廣泛,最古老的問題之一就是字符串匹配。關於字符串的模式匹配,《數據結構》教材中一般介紹兩種方法:一是“樸素的模式匹配算法”,另外一個是“快速模式匹配算法”,也就是KMP算法。

二,樸素匹配算法

樸素的模式匹配算法的基本思想是:逐個使用p中的字符去與t中的字符進行比較。

其中正文t的長度用n表示,模式字符串p的長度用m表示。如果t1=p1,t2=p2,…,tm=pm,則模式匹配成功,p1p2…pm即爲所要尋找的子串,此時返回其起始位置1即可;否則,將p向右移動一個字符,然後用p中的字符從頭開始與t裏面對應的字符一一比較[2]。

重複此操作直到匹配成功,或p已移動到這樣一個位置:t中剩餘字符數小於p長度,那麼就表明模式匹配不成功,t中沒有子串與p相等,我們約定返回-1。樸素模式匹配算法理解起來簡單,算法也易於實現,但因其執行效率低,最壞情況下時間複雜度爲O(nm)。分析該算法我們知道,效率低的原因在於,尋求匹配時,沒有充分利用部分匹配的結果,每次比較不匹配時,模式p總是隻能向右移動一個字符的位置,存在大量回溯。

三,KMP算法

在進行字符串比較時,能否在匹配不成功時不從頭開始匹配?部分匹配的信息可否記錄下來加以使用?要求不回溯,模式就需要向右滑動一段距離,那麼又如何確定滑動多遠的距離呢?

KMP算法解決了上述問題。

1.next數組
next[ j]指p[ j]字符前有多少個字符與p開頭的字符相同。KMP算法中,模式p部分匹配的信息記錄在next數組

中,因此next數組確定了模式p向右滑動的距離。next數組的定義、作用、數組元素的獲取和使用方法是字符串模式匹配章節講述的關鍵。
先看如下式子。

模式串p存在某個k(0<k<j),使得以下關係成立:
在這裏插入圖片描述
那麼next[j] = k ;
舉個例子:
模式p=abcabcd,j=6時,p0p1p2=p3p4p5,說明p[6]前面有3個字符與模式開頭的3個字符相同,所以有next[6]=3。
歸納一下,next[j]數組定義如下:
在這裏插入圖片描述
例子說明:
p=ababaaabab,next[j]數組爲 下圖所示:
在這裏插入圖片描述我們規定,next[0]=-1,next[1]=0(因p[1]前只有一個字符)。p[2]前的字符b和p開頭的字符a不同,故next[2]=0。p[3]前的字符a和p開頭的字符a相同,故next[3]=1。p[4]前的字符ab和p開頭的字符ab相同,故next[4]=2。p[5]前的字符aba和p開頭的字符aba相同,故next[5]=3。p[6]前只有一個字符a和p開頭的字符a相同,故next[6]=1。以此類推。

明白了next數組的含義,再來講解根據模式p求數組next值的程序,就容易理解了。求next數組的程序如下:

 public static int[] getIndexArray(char[] str2){
        if(str2.length == 1){
            return new int[]{-1};
        }
        int[] next = new int[str2.length];
        next[0] = -1 ;
        next[1] = 0 ;
        int i = 2 ;
        int cu = 0 ; // 原來最長匹配中的後面一個字符數字
        while(i < str2.length){
            if(str2[ i - 1] == str2[cu]){
                next[i++] = ++cu ; // 需要注意的是上次所匹配的最長字串的長度所對因的值  /
            } else if(cu > 0){
                cu = next[cu]; // 一直在不斷的減少
            } else {
                next[i++] = 0 ;
            }
        }
        return next;
    }
2.next數組的作用

設正文t=aaaaaab,模式p=aaab,求出模式p的next數組爲{-1,0,1,2},開始字符匹配,如所示:

					0		1		2		3		4		5		6
			t 	[ 	a		a		a		a		a		a		b	]
					|		|		|		x
			p	[	a		a		a		b	]
			
		next:	[	-1		0		1		2	]

到p[3]時,字符匹配失敗。使用樸素模式匹配算法,模式p向右移動一個字符,下一次匹配的字符爲t[1]與p[0]、t[2]與p[1].

					0		1		2		3		4		5		6
			t 	[ 	a		a		a		a		a		a		b	]
							|		|		|		x
			p			[	a		a		a		b	]
			
		next:			[	-1		0		1		2	]

而且,經過上一次的匹配,我們發現t[1]與p[1]相同,t[2]與p[2]相同,在next數組中我們知道p[3]=2,由求最長匹配的規律知,p[0]p[1]與p[1]p[2]相同,所以原本t[1]與p[0],t[2]與p[1]的匹配就可以轉化爲求t[1]與p[1],t[2]與p[2]的匹配。也就是可以進行最長子串的重疊操作 。

於是,第二次的匹配就可以直接從t[3]與p[2]開始.

					0		1		2		3		4		5		6
			t 	[ 	a		a		a		a		a		a		b	]
											|	
			p			[	a		a		a		b	]
			
		next:			[	-1		0		1		2	]

爲什麼我一定會確信,在t[3]和p[3]比較不匹配之後 ,就可以直接使用t[3]和p[2] ,而不是接着從 t[1]和p[0]開始比較。加速匹配的這個過程 。

假設,在第一次匹配失敗 之後 ,在進行t[3]和p[2]比較的過程中途,存在匹配成功的案例 , 由於在原來字符串中aaab中已經求出來的next數組的值,就是除去自己當前所指的字符之前的所有字符串最長前綴長度,如下圖
在這裏插入圖片描述
所以,在圖上這次匹配中,t[3] 和p[3]不匹配之後, 就可以將原來p字符串中的前綴 ,和t已經匹配的後綴進行對齊,也就是圖中p中的紅框和 t中的綠框進行對齊,也就是直接由上述過程加速到下圖過程:
在這裏插入圖片描述
假設 ,在加速的中途中還存在匹配成功的情況,但這是不可能的,因爲 , next數據中的數據已經是最長的前綴了,除了前綴和字符串的後綴匹配之外,中間過程的一切結果前綴不可能匹配成功。可以在紙上推演一下。

總結
  • kmp加速中串的匹配過程
  • 在一次匹配串和被匹配串不進行匹配的時候,就使用next 數組中的索引和 被匹配的串上次的索引進行對齊。
  • 必須先求出next數組 ,這是kmp的精髓所在。
代碼
/*
	[email protected]
*/
public static int getIndex(String str1 , String str2){
         char[] so =  str1.toCharArray();
         char[] de = str2.toCharArray();
         int[] next = getIndexArray(de);

         int p1 = 0 ;
         int p2 = 0 ;
         while( p1 < str1.length() && p2 < str2.length()){
            if(so[p1] == de[p2]){
               p1++;
               p2++;
            } else if(next[p2] == -1){
                p1 ++ ; // 代表一開始都是不相等的 。
            } else {
                p2 = next[p2] ; // 直接跳到next數組中的值
            }
         }
         return p2 == str2.length() ? p1 - str2.length() : -1 ;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章