講這兩算法之前,我們首先了解幾個概念:
串:又稱字符串,是由零個或多個字符組成的有限序列,如S="abcdef"。
子串:串中任意個連續的字符組成的子序列,稱為該串的子串,原串稱為子串的主串。如T="cde",T是S的子串。子串在主串中的位置,用子串的第一個字符在主串中出現的位置表示,T在S中的位置為3。
模式匹配:模式串的定位運算稱為串的模式匹配或串匹配。
假設有兩個串S,T,設S為主串,也稱正文串,T為子串(S包含T),也稱為模式串(與S進行匹配的串)。在匹配前,T稱為模式串更為合適,是因為T有可能不是S的子序列。
在主串S中查找與模式T相匹配的子串,如果查找成功,返回匹配的子串第一個字符在主串中的位置。最笨的辦法就是窮舉所有S的所有子串,判斷是否與T匹配。
時刻注意:不管是BF算法還是KMP算法,如果第一次比較就匹配了,程序自然就結束了。故關鍵是在出現不匹配時,如何確定下一輪誰與誰進行比較,即主串 i 的值Si與子串 j 的值Tj。
例如:S="abaabaabeca",T=" abaabe",求子串T在主串S中的位置, 其中用 i,j 分別表示S和T中正在進行匹配字符的位置。
1. 從 S 串第1個字符開始: i=1, j=1,比較兩個字符是否相等,如果相等,則 i++, j++;如果不等則執行第2步;

2. 從 S 串第2個字符開始:即 i 退回到 i- j+2 (i-(j-1)+1) 的位置,其中 j-1 的含義是不等之前比較的次數, 簡稱增量,i-增量意味着 i 回到初值,+1便是回溯到初值的下一位,
即 i=2, j=1,比較兩個字符是否相等,如果相等,則 i++, j++;如果不等則執行第3步;

3. 從 S 串第3個字符開始:即 i 退回到 i- j+2的位置,即 i=3, j=1,比較兩個字符是否相等,如果相等,則 i++, j++;如果不等則執行第4步;
4. 從 S 串第4個字符開始:即 i 退回到 i- j+2的位置,即 i=4, j=1,比較兩個字符是否相等,如果相等,則 i++, j++;由於此時 T 串比較完了,執行第5步;
5. 需要返回子串T在主串S中第一個字符出現的位置,j( i ) 移動了T.length次,即 i - T.length=10-6=4。因已匹配,不需要+1回溯到一開始位置的下個位置了。
其代碼如下:
int Index_BF(SString S, SString T) //返回模式串T在主串S中第一次出現的位置。因返回的是位置,不是狀態,雖都是int,但不用Status,另說清誰是模式串,誰是主串。
{
int i=1,j=1,sum=0; // i 指示主串S中進行匹配字符的位置,j 指示主串T中進行匹配字符的位置;sum用於累計比較次數;
while (i<=S.length&&j<=T.length)//看上面例子分析得出,匹配只有成功和不成功兩種情形,換成計算機語言也就是if else;
{
sum++;
if (S.ch[i]==T.ch[j]) //看上面例子,不管那一輪只要成功,那么有i++ ; j++
{
i++;
j++;
}
else //看上面例子,不管那一輪只要失敗,那么有i回溯到初值的下一位; j 始終是第1個和Si,即j=1;
{
i=i-j+2; //i=i-(j-1)+1,看上面例子中的解釋;
j=1;
}
}//循環完也只有兩種情形,要么匹配,要么失敗,換成計算機語言仍就是if else;
cout<<"BF一共比較了"<<sum<<"次"<<endl;
if(j>T.length)
return (i-T.length); //返回位置. 比較成功的次數是T.length, i從初值也后移了T.length,i-T.length回到初值
else
return 0;
}
上述算法稱為 BF(Brute Force) 算法,Brute Force的意思是蠻力,暴力窮舉。其時間復雜度最壞達到O(n*m),n,m分別為S、T串的長度。
實際上,完全沒必要從S的每一個字符開始,暴力窮舉每一種情況,Knuth、Morris和Pratt對該算法進行了改進,稱為KMP算法。
我們再回頭看剛才的例子:
從S串第1個字符開始:i=1,j=1,比較兩個字符是否相等,如果相等,則i++,j++;按照BF算法,如果不等則i退回到i-j+2的位置,即i=2,j=1。

其實 i 不用回退,讓 j 回退到第3個位置,接着比較即可(KMP算法);

是不是像T串向右滑動了一段距離?為什么可以這樣?為什么讓 j 回退到第3個位置?而不是第2個,第4個?
因為T串中開頭的兩個字符 和 i 指向的字符前面的兩個字符一模一樣噢,那 j 就可以回退到第3個位置繼續比較了,因為前面兩個字符已經相等,不用比較了,如下圖。

問題來了, 那我們怎么知道T串中開頭的兩個字符和 i 指向的字符前面的兩個字符一模一樣?難道還要比較?
分析發現: i 指向的字符前面的兩個字符 和 T串中 j 指向字符前面的兩個字符一模一樣,因為它們一直相等,才會i++,j++走到后面的位置,不僅主串字串這兩個字符一樣,ij之前所有字符也一樣(S1S2……Si-1=T1T2……Tj-1)。

也就是說,我們不必判斷T串中開頭的兩個字母和S串中 i 指向的字符前面的兩個字符是否一樣,只需要在T串本身比較就可以了。即T′的前綴T1T2和T′的后綴Tj-2Tj-1比較即可(T':T1T2……Tj-1):

判斷T′="abaab"的前綴和后綴是否相等,找相等前綴后綴的最大長度 l 。
長度為1的:前綴"a",后綴:"b",不等×
長度為2的:前綴"ab",后綴:"ab",相等√
長度為3的:前綴"aba",后綴:" aab",不等×
長度為4的:前綴"abaa",后綴:"baab",不等×
注意:前綴和后綴不可以取字符串本身,如果取了,不就是原地踏步嗎。串的長度為5,前綴和后綴長度最多達到4(l <5)。
相等前綴后綴的最大長度為l =2,則 j 就可以回退到第l +1=3個位置繼續比較了( i 不變)。
現在我們可以寫出通用公式,next[j]表示 j 可以回退的位置,T′="T1T2…Tj-1",則:
那么我們很容易求出T="abaabe"的next[j]數組:

解釋1:
j=1:根據公式next[1]=0;
j=2:T′="a",沒有前綴和后綴,next[2]=1;
j=3:T′="ab",前綴為"a",后綴為"b",不等,next[3]=1;
j=4:T′="aba",前綴為"a",后綴為"a",相等且l=1;前綴為"ab",后綴為"ba",不等,next[4]=l+1=2;
j=5:T′="abaa",前綴為"a",后綴為"a",相等且l=1;前綴為"ab",后綴為"aa",不等;前綴為"aba",后綴為"baa",不等,因此next[5]=l+1=2;
j=6:T′="abaab",前綴為"a",后綴為"b",不等;前綴為"ab",后綴為"ab",相等且l=2;前綴為"aba",后綴為"aab",不等;前綴為"abaa",后綴為"baab",不等,取最大長度2,因此next[6]=l+1=3。
這樣找所有的前綴和后綴比較,是不是也是暴力窮舉?那怎么辦呢?Look……
用動態規划遞推一下(數學歸納法,已知n=1成立, 假設n成立,來證明n+1是否成立):
首先大膽假設,已知next[j]=k(求next[j+1]=?),其當前含義是:下一次匹配時, j=next[j]=k, 后判Si==Tk?,意味着子串中,j之前有k-1個字符前后綴匹配,才會有next[j]=k-1+1=k:
那么next[j+1]=? 回想 P97 圖4.8,已知 next[j](為表述簡潔,不妨令k=next[j]), 如何求next[j+1]? 思考 j=5及j=6:

考察以下兩種情況:
-
-
-
tj=tk:那么 next[ j+1]=next[j]+1=k+1,即,在j+1前,相等前綴和后綴的長度比next[j]=k多1,eg:j=5。
-
-

-
-
-
tj≠tk:當兩者不相等時,我們又開始了這兩個串的模式匹配,找 tj 與 next[k] 位置的 tk′比較 (j不變,k變,往前回溯到k'=next[k])。注:程序中的處理,只需要把 next[k] 賦值給 k,即 k=next[k],不用新變量k'。
-
-
如果tj=tk' 則 next[j+1]=next[k]+1=k'+1;

如果tj≠tk',則繼續向前找next[k′](k''=next[k']),如果還不相等,繼續向前找next[k′'](k'''=next[k'']),直到找到next[1]=0,停止,此時next[j+1]=next[1]+1=0+1=1,即從第一個字符開始,j=1, eg: j=6。
求解next步驟(當前j,求j+1):
首先,第一位的next值直接賦0,第二位的next值直接賦1;
其次,后面求解每一位的next值時(不妨令求j+1),都要前一位(T.ch[ j ])與其next值對應位(T.ch[ next[ j ] ])進行比較。若相等,則該位的next值就是前一位的next值加上1(next[ j ]+1);若不等,繼續重復這個過程(T.ch[ j ]==T.ch[ next[ next[ j ] ] ]),直到找到相等某一位,將其next值加1即可。如果找到第一位也都沒有找到,那么該位的next值即為1。 
求解next[]的代碼實現如下:
void Get_Next(SString T,int next[])//求模式串T的next[]函數值,其實是知next[1],求next[2],依次……,也即假設已知next[j], ++j后求得next[j]即未我所求;
{
next[1] = 0; //初值
int k, j;
j = 1; // j 代表的是后綴末尾的下標, 假設next[j]已知,則 ++j 后,next[j]即是所求;
k = 0; // k代表的是前綴結束時的下標,也就是 j 前有k個字符的前綴T1T2...Tk等於后綴Tj-m+1...Tj
while (j < T.length)
{
if (k == 0 || T.ch[k] == T.ch[j]) //1. j>=2時, 怎么求next[j+1]?next[3]有前綴后綴,相等->,不等->看上面例子2. 當j=1時,怎么求next[j+1]?前綴個數為0->不等<==>k==0后++j;++k;next[j] = k;
{
++j;
++k;
next[j] = k; //++j后才是我們想要的next[j],++k后才是我們想要給next[j]賦的值;一定要先++
}
else //匹配失敗的情況,就要進行回溯,下一輪tj與tk'比較,j不動,k'=next[k];
k= next[k];
}
}
用上述方法再次求解求出T="abaabe"的next[]數組:
解釋2:
1. 初始化時next[1]=0,j=1,k=0,進入循環,判斷滿足k==0,則執行next[++j]=++k,即next[2]=1,此時j=2,k=1;
2. 進入循環,判斷滿足T.ch[j]==T.ch[k],T.ch[2]≠T.ch[1],則執行k=next[k],即k=next[1]=0,此時j=2,k=0;
3. 進入循環,判斷滿足k==0,則執行next[++j]=++k,即next[3]=1,此時j=3,k=1;
4. 進入循環,判斷滿足T.ch[j]==T.ch[k],T.ch[3]=T.ch[1],則執行next[++j]=++k,即next[4]=2,此時j=4,k=2;
5. 進入循環,判斷滿足T.ch[j]==T.ch[k],T.ch[4]≠T.ch[2],則執行k=next[k],即k=next[2]=1,此時j=4,k=1;
6. 進入循環,判斷滿足T.ch[j]==T.ch[k],T.ch[4]=T.ch[1],則執行next[++j]=++k,即next[5]=2,此時j=5,k=2;
7. 進入循環,判斷滿足T.ch[j]==T.ch[k],T.ch[5]=T.ch[2],則執行next[++j]=++k,即next[6]=3,此時j=6,k=3;
8. j=T.length,循環結束。
- 結果是不是和窮舉前綴后綴一模一樣,解釋1(窮舉)同解釋2(歸納)的結果?
有了next[]數組,就很容易進行模式匹配KMP,當S.ch[i]≠T.ch[j]時,j回溯到next[j]的位置 繼續和 S.ch[i]比較即可。
這樣求解非常方便,但也發現有一個問題:當S.ch[i]≠T.ch[j]時,j退回到next[j],然后S.ch[i]與T.ch[k]比較。這樣的確沒錯,但是如果T.ch[j]=T.ch[k], 這次比較就沒必要了,因為我們剛知道S.ch[i]≠T.ch[j]啊,
那么肯定S.ch[i]≠T.ch[k],完全沒必要再比了 (S.ch[i]≠T.ch[j]=T.ch[k]<==>S.ch[i]≠T.ch[k]).
再向前找下一個next[],即找可k'=next[k]的位置,繼續比較就可以了。本來應該和第k個位置比較呢,相當於跳到了k的上一個位置k',減少了一次無效比較。
求解next-val步驟( 當前j,求j ):
- next-val數組第一個值直接賦0;
- next-val第二數:模式串第二個字符為B,對應的下標數組第二個數是1,那就是將模式串的第1個字符和B相比較,A!=B,所以直接將下標數組第二個數1作為next-val數組第二個數的值;
- 第三個數:模式串第三個字符為A,對應下標數組第三個數為1,取其作為下標,找到模式串第1個字符為A,A=A,那取next-val的第一個數做為next-val第三個數的值,也就是0.
| 位置 |
1
|
2
|
3
|
4
|
5
|
6
|
7
|
|---|---|---|---|---|---|---|---|
| 模式串 |
A
|
B
|
A
|
C
|
A
|
B
|
C
|
| next數組 |
0
|
1
|
1
|
2
|
1
|
2
|
3
|
| nextval數組 |
0
|
1
|
0
|
2
|
0
|
1
|
3
|
修改next[]程序:
求解nextval[]的改進代碼實現如下:
void Get_Nextval(SString T,int nextval[])//求模式串T的nextval[]函數值,在next函數的基礎上只需改動tj=tk相等時的情形,讓nextval[j]=nextval[k]而不是k ;
{
nextval[1] = 0; //初值
int k, j;
j = 1; // j 代表的是后綴末尾的下標, 若next[j]已知,則 ++j 后,next[j]即是所求;
k = 0; // k代表的是前綴結束時的下標,也就是 j 前有k個字符的前綴T1T2...Tk等於后綴Tj-m+1...Tj
while (j < T.length)
{
if (k == 0 || T.ch[k] == T.ch[j]) //1. 當j=1時,怎么求next[2],也即next[++j]?前綴個數為0,特殊值法知next[2]=1<==>k==0;2. j>=2時,已知next[j], 怎么求next[j+1]?eg. aaaab
{
++j;
++k;
if(T.ch[k]==T.ch[j])
nextval[j]=nextval[k]; //相等了意味着Si無需和Tk在比較了,回溯到k的上一個位置k'=next[k],讓Si和Sk'比較
else
nextval[j]=k; //k其實就是next[j]. 當T.ch[k]!=T.ch[j],相當於nextval[++j]=++k,也即nextval[j]=next[j]=k,此情形同next[]函數;
}
else//匹配失敗的情況,就要進行回溯,下一輪tj與tk'比較,j不動,k'=next[k];
k= nextval[k];
}
}
/***KMP及改進算法***/
int Index_KMP(SString S,SString T,int next[])//利用非空模式串T的next函數求T在主串S中的位置的KMP算法,形式上近同BF算法
{
int i=1,j=1,sum=0;//i-->S,j-->T,sum-->循環次數
while(i<=S.length&&j<=T.length)
{
sum++;
if(j==0||S.ch[i]==T.ch[j]) // 繼續比較后面的字符
{
i++;
j++;
}
else
j=next[j]; // 利用next函數確定下一次模式串中第j個字符與T.ch[i]比較,i不變
}
cout<<"KMP一共比較了"<<sum<<"次"<<endl;
if(j>T.length) // 匹配成功
return i-T.length;
else
return 0;
}
總結:
BF算法中:當主串和子串不匹配的時候,主串和子串你的指針都須要回溯,因此致使了該算法時間復雜度比較高為 O(n*m) ,空間復雜度為 O(1) 。注:雖然其時間復雜度為 O(n*m) 可是在通常應用下執行,其執行時間近似 O(n+m) 因此仍被使用。
KMP算法:利用子串的結構類似性,設計next數組,在此之上達到了主串不回溯的效果,大大減小了比較次數,可是相對應的卻犧牲了存儲空間,KMP算法時間復雜度為 O(n+m) 空間復雜度為 O(n)。
————————————————
在原文的基礎上,加上自己的理解給大家解釋下,原文鏈接:https://blog.csdn.net/rainchxy/article/details/78130155
