傳統的字符串匹配模式(暴力循環)
子串的定位操作通常稱作串的串的匹配模式,也就是在主串S中查找模式串(子串)T第一次出現的位置。如比較以下兩個串:
主串S:ABCDABX
子串T:ABX
我們可以通過暴力循環的方式依次的比較S[i]和T[j],若匹配失敗,則子串向前移位1步,重新開始匹配,直至匹配完成。
主串S:ABCDABX
子串T: ABX(匹配成功)
傳統的暴力循環代碼如下:
int index(String S,String T){
int i,j;
i = j = 0;
while(i<StrLength(S) && j<StrLength(T)){
if(S[i] = T[i]){
i++;
j++;
} //繼續比較后續字符
else{
i = i - j + 1;
j = 0;
} //指針后退重新開始匹配
}
if(j > StrLength(T)) return i - StrLength(T) + 1; //返回定位
return 0; //匹配失敗
}
這種傳統的模式匹配方式最壞的情況下需要循環mxn次,時間復雜度為O(mxn),因為主串中可能存在多個和模式串“部分匹配”的子串,因此指針多次回溯,效率極低。
KMP算法的匹配過程
KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,因此人們稱它為克努特——莫里斯——普拉特操作(簡稱KMP算法)。KMP算法的關鍵是利用匹配失敗后的信息,盡量減少模式串與主串的匹配次數以達到快速匹配的目的。用以下例子說明:
- 主串:abcababca...(假設主串很長,我們就先看前9位)
- 子串:abcabx
按照傳統的匹配模式的過程就應該如下:
傳統的匹配模式,就應該是按照上面的方式一步一步的匹配下來,一旦匹配失敗,主串指針i就要回溯,效率非常低!而KMP算法的匹配過程只需要兩步:
為什么一下次就可以跳過中間的比較來到這一步呢,下面就來探究KMP算法的匹配方式。
先來對比一下傳統的匹配模式,可以發現主串的指針i值的變化:
第一次遍歷到了i=6,匹配失敗;
第二次遍歷到了i=2,匹配失敗;
第三次遍歷到了i=3,匹配失敗;
第四次遍歷到了i=4,匹配失敗;
第五次遍歷到了i=5,匹配失敗;
直到第五次i值終于又回到了i=6。
i值的變化情況:6->1->2->3->4->5->6
在傳統的匹配算法中,可以發現i值是不斷回溯的。
反觀KMP算法,只需對主串一次遍歷,i值不會回溯,即遍歷過程中i值是不會變小的。
那么既然KMP算法的i值遍歷只需一次,那么就要考慮j是如何變化的了,為什么第一次匹配失敗后j可以從j=3開始匹配,而不像傳統遍歷算法那樣每當匹配失敗就要從j=1重新開始匹配。
再看看一開始對KMP算法的定義:KMP算法的關鍵是<u>利用匹配失敗后的信息</u>,盡量減少模式串與主串的匹配次數以達到快速匹配的目的。
劃重點:<u>利用匹配失敗后的信息</u>。什么是匹配失敗后得到的信息呢?
于是回到剛剛的第一次匹配,看看能從這次失敗的匹配中得到什么信息。
因為S[1...5] = T[1…5] 所以有 S[1,2] = T[1,2] S[4,5] = T[4,5]
又因為子串T有 T[1,2] = T[4,5],所以S[4,5] = T[1,2]
那下一次滑動到直接讓S[4,5] = T[1,2],然后繼續比較下一個元素就行啦。
這是簡化模型第一次匹配的情況,根據傳統的匹配算法,當匹配失敗時模式串T移動一格,和S串比較。但是由于綠色部分在第一次匹配的時候發現了額外的信息:
就像剛剛那個例子,T[1,2] = S[4,5],都這樣了,難道T還需要一格格的移動嗎,直接滑過去就行啦。
這就是KMP算法的匹配過程。
如何確定模式串的滑動區間
知道了KMP算法的匹配過程,接下來就要考慮計算機是如何知道匹配失敗時,指針j下一次指向的位置。由于KMP算法中指針i是不減的,因此j的指向位置只與模式串本身的結構有關。j的滑動位置的信息存放在next數組中。當匹配失敗,就可以通過查詢next數組的值得到下一次j滑動的位置。
next數組存放的是模式串的移位信息,具體就是模式串的部分匹配值,next數組大小與模式串T等長。
部分匹配值"就是"前綴"和"后綴"的最長的共有元素的長度。以"ABCDABD"為例,
- "A"的前綴和后綴都為空集,共有元素的長度為0;
- "AB"的前綴為[A],后綴為[B],共有元素的長度為0;
- "ABC"的前綴為[A, AB],后綴為[BC, C],共有元素的長度0;
- "ABCD"的前綴為[A, AB, ABC],后綴為[BCD, CD, D],共有元素的長度為0;
- "ABCDA"的前綴為[A, AB, ABC, ABCD],后綴為[BCDA, CDA, DA, A],共有元素為"A",長度為1;
- "ABCDAB"的前綴為[A, AB, ABC, ABCD, ABCDA],后綴為[BCDAB, CDAB, DAB, AB, B],共有元素為"AB",長度為2;
- "ABCDABD"的前綴為[A, AB, ABC, ABCD, ABCDA, ABCDAB],后綴為[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度為0。
下面是模式串T:a b c d a b c a | next數組的推導過程
j i
模式串:a b c d a b c a
串下標:0 1 2 3 4 5 6 7
next :0 0
T[j]≠T[j] i++ next[i] = 0
j i
模式串:a b c d a b c a
串下標:0 1 2 3 4 5 6 7
next :0 0 0
T[j]≠T[j] i++ next[i] = 0
j i
模式串:a b c d a b c a
串下標:0 1 2 3 4 5 6 7
next :0 0 0 0
T[j]≠T[j] i++ next[i] = 0
j i
模式串:a b c d a b c a
串下標:0 1 2 3 4 5 6 7
next :0 0 0 0 1
T[j]=T[j] i++ j++ next[i] = j+1
j i
模式串:a b c d a b c a
串下標:0 1 2 3 4 5 6 7
next :0 0 0 0 1 2
T[j]=T[j] i++ j++ next[i] = j+1
j i
模式串:a b c d a b c a
串下標:0 1 2 3 4 5 6 7
next :0 0 0 0 1 2 3
T[j]=T[j] i++ j++ next[i] = j+1
j i
模式串:a b c d a b c a
串下標:0 1 2 3 4 5 6 7
next :0 0 0 0 1 2 3
T[j]≠T[j] j = next[j-1]
j i
模式串:a b c d a b c a
j :0 1 2 3 4 5 6 7
next :0 0 0 0 1 2 3 1
T[j]=T[j] next[i] = j+1
最后得到 next[] = {0,0,0,0,1,2,3,1}
C語言的next數組實現如下:
void get_next(String T,int next[]){
int j = 0;
int i = 1;
next[0] = 0;
while(i<StrLength(T)){
if(T[i] == T[j]){
next[i] = j + 1;
++j;
++i;
}
else{
if(j!=0){
j = next[j-1];
}
else{
next[i] = 0;
++i;
}
}
}
}
有了next數組,我們就可以知道每當KMP匹配過程中,一旦匹配失敗,我們就令指針 j = next[j-1] ,然后繼續與S[i]比較。
KMP完整算法如下:
int KMP(String S,String T){
int length_S = StrLength(S);
int length_T = StrLength(T);
int next[length_T];
get_next(T,next);
int i = 0;
int j = 0;
while(j<length_T && i<length_S){
if(T[j] == S[i]){
++i;
++j;
}
else{
if(j!=0)
j = next[j-1];
else
++i;
}
}
if(j==length_T) return i - length_T +1;
return 0;
}
文章參考
《大話數據結構》
《數據結構》—嚴蔚敏