KMP的自我理解
KMP算法的核心思想為,當文本串與模式串在某一位置發生失配時,利用已經匹配部分的信息,讓模式串迅速向后移動,以完成快速匹配。
很重要的一點
模式串快速后移多少個單位,或者說失配后,文本串應該繼續和模式串中的哪個字符繼續比對,也就是常說的next[j]的值,這個值是只與模式串有關,而與文本串是無關的。
為什么只與模式串有關
注意:下面討論時,P[0,j)為左閉右開區間,包括下標為0不包括下標為j的字符,而P[0,j]則表示閉區間。
設文本串T和模式串S,假設在T[i]與P[j]處發生失配時,此時,我們已經掌握了文本串T[i-j,j)這部分子串的全部信息,KMP算法就是要利用這部分已知信息,快速確定T[i]應該繼續和模式串中的哪個字符進行比對。
值得慶幸的是,這部分信息和P[0,j)是完全一致的。因為在比對T[i]與P[j]時,說明T[i-j,i)和P[0,j)已經是匹配成功的,才有繼續往后比對的必要。
因此,我們可以根據模式串,提前計算出P[j]與文本串某個字符發生失配時,繼續使用模式串哪個位置的字符與文本串進行比對,這個值就是next[j]。因此,KMP算法的核心就是提前計算出在模式串某位置j發生失配時,應該跳到哪個位置繼續比對,這些位置組合起來,就是next數組。
注意:next[j]的值表示,當模式串P{j]與文本串T[i]發生失配時,使用P[next[j]]代替P[j]繼續和T[i]進行下一次比對。
next數組頭腦風暴
當T[i]與P[j]發生失配時,則利用next數組獲得下一個應該比對的位置,繼續和T[i]進行比對即可。
T[i]與P[j]失配時,拿到next[j],讓P[next[j]]繼續和T[i]進行比對,當然如果還是失配,那么繼續和P[next[next[j]]]進行比對,一直這樣迭代下去
按上述思路一直迭代下去,最終必然會出現兩種情況:
2.1 j一直往前迭代,找到了某個位置j',使得P[j']=T[i],那么此時可以確定T[i-j', i]和P[0,j']已經匹配,二者一起后移,繼續比對T[i+1]和P[j'+1],這又是新一輪的比對
2.2 j一直往前迭代,都沒有找到和T[i]一致的字符,此時,j'一定會越界(程序中一般設置為-1),這種情況,說明找不到和T[i]對應的字符,應該跳過T[i],讓T[i+1]和P[0]開始新一輪的比對
上述第二種情況,和T[i]與P[0]比對后不一致是類似的,此時,首字母不匹配,直接拋棄T[i],讓T[i+1]和P[0]開始新一輪的比對
利用假想哨兵統一上述兩種情況
可以假想在模式串的下標為-1處有一個通配哨兵,這個哨兵與任何字符都是匹配的。當T[i]與P[0]失配時,繼續往前,讓T[i]與P[-1]進行比對,此時,必然比對成功,按照2.1的思路,即找到j'=-1,這樣,二者一起后移,繼續比對T[i+1]和P[j'+1],即T[i+1]和P[0],這樣,便可以將2。2統一到和2.1一致的思路中
注意:利用哨兵時,next[0]必須為-1,這樣,在越界時,使用2.1中的思路,找到的j'是-1,,下輪比對才會恰巧是T[i+1]和P[0],因此,next[0]=-1也是next數組構造的初始條件
已知next數組情況下,進行KMP模式匹配的代碼
/*KMP匹配@param: T 文本串@param: P 模式串@return: 失敗返回-1,成功則返回模式串在文本串的起始下標 */ int kmpMatch(const char* T, const char* P){int len = strlen(T);int patternLen = strlen(P);//生成next數組 int* next = new int[patternLen];getNext(next, P);int i = 0, j = 0;//i<len,表示還能繼續匹配//j<patternLen,表示還沒匹配成功 while(i < len && j < patternLen){//j<0相當于匹配到通配哨兵//T[i] == P[j]則表示當前比對通過//這兩種情況都為比對通過,文本串和模式串一起后移,繼續比對T[i+1]和T[j'+1] if(j < 0 || T[i] == P[j]){++i;++j;}else{//按照2.2的思路,若找不到合適的j',則一直利用next數組往前迭代,直到找到相等的或者匹配到哨兵(哨兵是通配的) j = next[j];}}delete[] next;//由于串長為patternLen,那么比對成功的起始位置不可能超過Len-patternLen if(i-j > len-patternLen){return -1;}return i-j; }next數組的構造
再次強調,next數組的構造只和模式串有關
next[j]的值表示P[j]與T[i]失配后,使用P[nex[j]
繼續和T[i]進行新一輪的比對
由于next數組的構造理解起來較為困難,因此先通過一個例子找一下規律:
設模式串P="aabbccaabbd",在下標為10的字母d處發生失配時,此時我們能掌握前綴P[0,10)信息,分析這個前綴,可以發現它的前綴"aabb"和它的后綴"aabb"是完全一致的,那么我們可以直接讓P[4]替代P[10]繼續下一輪的比對,在這個例子中,next[10]=4
仔細思考和分析,可以總結出這個規律:在P[j]處發生失配時,分析該位置之前的子串,找到它的最長公共前后綴,那么這部分公共前后綴的信息是可以不用比對的,可以讓前綴的下一個字符替代P[j]和T[i]進行下一輪的比對
再注意一個微妙的地方:由于字符串的下標從0開始,因此最長公共前后綴的長度恰好就是下一個應該比對的字符的位置,因為長度正好等于最后一個字符位置+1
例如:前面的例子中,對于模式串P="aabbccaabbd",在P[10]處發生失配時,P[0,10)的最長公共前后綴為"aabb",其長度為4,而前綴"aabb"的最后一個字符b在字符串中的位置為3,那么跳過前綴,下一個比對的位置正好應該是3+1=4,其恰好也為字符串的長度4。
但是我們不可能對于模式串的每個位置,都直接去計算最長公前后綴,而是利用模式串自相似的特性,快速構造next數組
注意構造next數組的過程和KMP使用next的過程是類似的,構造模式串的過程就像是利用已經求的的前半部分的next數組,讓前綴去匹配后綴,求出最新的next[j],一直這樣往后迭代
因此,我們構造next數組都是基于已知的next[0,j],求next[j+1]的迭代過程:
- 當P[j]=P[next[j]時,那么P[0,j]的最長公共前后綴會在原來的基礎上+1,即next[j+1] = next[j] + 1
- 當P[j]!=P[next[j]]時,設next[j]=j',可以理解為這是一次前綴P[0,j']和后綴P[j-j', j]失配的過程此時P[j-j',j]相當于文本串,那么,按照前面的思路,應該讓j'繼續王錢迭代,知道找到P[j']=P[j],此時按照和上面相同的處理思路即可
next數組構造的模擬
設模式串P="aabbccaabbd",len=11,下標為0-10,設變量i為已求出next值的最后一個下標,而j即為前綴P[0,i)的最長公共前后綴長度,也就是在P[i]處失配時替換的位置,即j=next[i]
初始化:i=0,j=-1,next[0]=-1,表示首字母失配時,和哨兵比對
注意:i為已經求出next值的最后一個位置,并利用next[0,i]的信息求解next[i+1],因此i<len-1. i+1最大為len-1。
其實1-3是遞歸求解可以匹配的j的過程,其過程和已知next數組進行KMP模式匹配的過程是一致的,只不錯這里前綴充當模式串,后綴充當文本串
i=7,j=1,欲求next[8]:
其實next數組的迭代求解其實是利用了這樣一個規律:當前僅當P[j]==P[next[j]]時, 這P[0,j+1]的最長公共前后前綴包含了P[0,j]的最長公共前后綴,且長度為P[0,j]的最長公共前后綴+1;若是不同,則說明P[0,j+1]的最長公共前后綴不包括P[0,j]的最長公共前后最,但可能包括P[0,next[j]]的最長公共前后綴,因此要一直往前找,知道找到相等的或者匹配到哨兵為止。
構造next數組C++版本
/*構造next數組 */ void getNext(int* next, const char* P){next[0] = -1;int len = strlen(P);int i = 0;int j = -1;while(i < len-1){//匹配到哨兵或者相等 if(j < 0 || P[i]==P[j]){next[++i] = ++j;}else{//遞歸往前搜索 j = next[j];}} }KMP算法的優化
上述的next數組的構造方式其實還是有可優化的空間的,我們來看一個極端的例子:
設模式串T="aa",則next[0]=-1,i=0,j=-1,判斷P[0]==P[-1],是,那么next[++i]=++j,即next[1]=-1+1=0
問題就在于這個next[++i]=++j,next[1]=-1+1=0
首先,next[1]等于0表示,當P[1]與文本失配時,用P[0]替代P[1]繼續和文本串進行比對,但毫無疑問,這次比對必然會失配,因為P[0]和P[1]是相等的,這樣我們做了一次多余的比對后,讓程序繼續j=next[0]=-1;
由于我們在構造next數組時,求的最長公共前后綴的長度后,便直接將這個值作為下一個比對的位置(next[++i]=++j,這里的++j其實就是最長公共前后綴的長度),卻不管這個位置上的字符是否已經和當前確定失配的字符是一樣的,因此,才會多出來這些多余的比較
那前面的T="aabbccaabbd"來說,其原有的next數組為[-1,0,1,0,0,0,0,1,2,3,4],假設我們在第9個字符b處發生失配,求的P[0,8]的最長公共前后綴為3,我們便直接另next[9]=3,卻不管P[3]是否和P[9]相等,而在這個例子中P[3]恰好等于P[9],P[9]失配則毫無疑問P[3]一定會失配,程序繼續使用next[3]的值代替P[3]繼續往前搜索。
但是,我們如果在構造next的過程中,提前判斷P[next[j]]是否等于P[j],若相等,則跳過next[j]位置的比對,直接使用next[next[j]]處的值替代next[j],這樣,便可以避免此次比對
如上述例子:j=9,原next[9]=3,由于P[3]==P[9],直接跳過P[3],使用next[3]代替3,這樣便可以跳過很多不必要的比對
next數組構造的優化
/*構造next數組 */ void getFastNext(int* next, const char* P){next[0] = -1;int len = strlen(P);int i = 0;int j = -1;while(i < len-1){if(j < 0 || P[i]==P[j]){++i; ++j;if(P[i]!=P[j]){//若和當前已經失配的字符不相等,直接使用最長公共自前綴賦值 next[i] = j;}else{//若和當前已經失配的字符相等,則跳過本次比對,直接往前 next[i] = next[j];}}else{j = next[j];}} }轉載于:https://www.cnblogs.com/tommychok/p/9029082.html
總結
- 上一篇: python3 tkinter
- 下一篇: 0428专题:行内元素与块状元素