在字符串模式匹配的學習中,對於沒有學過的數據結構與算法的來講,可能首先就會想起將模式字符串和目標字符串逐個去比較,直到匹配為止,這就學術上說的“朴素”算法,這算法的確可行,但是不高效,從而有了KMP的算法的出現,簡單來講KMP算法就是利用模式字符和匹配過程的已知條件得出一個值,去跳過在朴素算法逐個匹配過程中無必要的匹配,從而達到高效的算法。雖然這是簡單的思路,但是KMP算法理解起來真的比較費勁,下面,我自己結合課件和網上各位大神的解釋,總結寫一下比較好懂的KMP算法解釋。
字符串模式匹配指的是,找出特定的模式串在一個較長的字符串中出現的位置。
- 朴素的模式匹配算法(BF(Brute Force)算法)
朴素模式匹配算法的基本思想是窮舉法,即就是將目標串S的第一個字符與模式串P的第一個字符進行匹配,若相等,則繼續比較S的第二個字符和P的第二個字符;若不相等,則比較S的第二個字符和P的第一個字符,依次比較下去,直到得出最后的匹配結果(如圖1所示)。
圖1:朴素的匹配方法示意
朴素的字符串模式匹配算法:
- /*
- 函數 int NaiveStringMatching(String T,String P)從目標T的首位位置開始模式匹配,
- 用模式P匹配T,尋找首個P子串並返回其下標位置。若整個匹配過程失敗 (模式P移動到目標T
- 尾部位置),則返回負值。
- */
- int NaiveStringMatching(const String &T,const String &P){
- int i=0; //模式的下標變量
- int j=0; //目標的下標變量
- int pLen=P.length(); //模式的長度
- int tLen=T.length(); //目標的長度
- if (tLen<pLen) //如果目標比模式短,匹配無法成功
- return(-1);
- while (i<pLen&&j<tLen){ //反復比較對應字符來開始匹配
- if (T[j]==P[i])
- i++,j++; //繼續比較后續字符
- else{ //指針回溯到 下一首位,重新開始
- j=j-i+1;
- i=0;
- }
- }
- if (i>=pLen)
- return (j-pLen+1);
- else
- return (-1);
- }
上面的代碼,T就是目標串,p是模式串,其實現思想也很簡單:
當T[j] == p[i]時,目標串和模式串的指針都向后移動一位,進行匹配。而當T[j] != p[i]時,即匹配不成功時,將目標串和模式串的指針同時回溯,i = 0 而目標串的指針j則回溯到這輪開始的下一個位置。
朴素的模式匹配的算法復雜度是O( (n-m+1) * m) n為目標串的長度,m為模式串長度。
從其實現思想上可以很容易的看出,造成該算法低效的地方是在匹配不成功時主串和模式串的指針回溯上。
- KMP模式匹配算法
為了避免指針的回溯,Knuth(D.E.Knuth)、Morris(J.H.Morris)和Pratt(V.R.Pratt)等人,發現其實每次右移的位數存在且與目標串無關,僅僅依賴模式本身,從而進行改進算法。
改進后的算法(簡稱為:KMP算法)的基本思想為:預先處理模式本身,分析其字符分布狀況,並為模式中的每一個字符計算失配時應該右移的位數。這就是所謂的字符串的特征向量。
字符串的特征向量是KMP算法的關鍵,而這個字符串的特征向量也稱為Next數組,所以如果我們可以得出這個Next數組就可以知道每一個字符失配時應該右移的位數。
而問題來了,這個所謂的Next數組(字符串的特征向量)怎么樣可以求出?在解決這個問題之前,我們先來理解一下字符串的“前綴子串”和“后綴子串”兩個概念。
- "前綴子串"指除了最后一個字符以外,一個字符串的全部頭部組合
- "后綴子串"指除了第一個字符以外,一個字符串的全部尾部組合。
同時我們還來定義"前綴子串"和"后綴子串"的最長的共有元素的長度為K值,稱為特征數,以"ABCDABD"為例:
1、 "A"的前綴和后綴都為空集,共有元素的長度為0;
2、 "AB"的前綴為[A],后綴為[B],共有元素的長度為0; 
3、 "ABC"的前綴為[A, AB],后綴為[BC, C],共有元素的長度0;
4、 "ABCD"的前綴為[A, AB, ABC],后綴為[BCD, CD, D],共有元素的長度為0;
5、 "ABCDA"的前綴為[A, AB, ABC, ABCD],后綴為[BCDA, CDA, DA, A],共有元素為"A",長度為1;
6、 "ABCDAB"的前綴為[A, AB, ABC, ABCD, ABCDA],后綴為[BCDAB, CDAB, DAB, AB, B],共有元素為"AB",長度為2;
到目前為止,應該可以理解特征數K值的求法,而所有的特征數組成就成為我們所求的Next數組(字符串的特征向量)。細心的可以發現模式串P "ABCDABD"中第一“A”下面沒有對應的特征數K值。其實此可以填上-1作為特征數,進而可以組成特征向量Next數組={-1,0,0,0,0,1,2},至於為什么是”-1“,而不是其他數呢?下面會有說明。
在說明為什么是”-1“之前,先驗證之前說的Next數組就可以知道每一個字符失配時應該右移的位數,下面舉一例子以說明:
如果按照之前所說得朴素模式匹配算法的話,最自然的反應是,將搜索詞整個后移一位,再從頭逐個比較。
但是,我們已經在上面已經對模式串P進行分析了,還得出了Next數組(字符串的特征向量),不必再去逐個比較,
已知空格與D不匹配時,前面六個字符"ABCDAB"是匹配的。由失配的”D”對應的特征數K值為2,對於的模式串位置為6,即可按照以下公式可算出向后移動的位數:
| 移動位數 = 失配字符所在的位置i - 對應特征數K |
即可移動位數=6-2=4,所以應該向后移動 4 位。同理,當失配時,即可計算出移動位數,直到匹配為止。
此刻根據公式,即可反向推出為什么首字符對應為“-1”,可以假設當模式串P與目標串一開始就不匹配,易知需要移動一位,
即1(移動位數)=0(失配字符所在的位置i)-對應特征數K
得對應特征數K=-1;可見為什么是“-1”即可得出。
由此可以得出模式P的特征向量Next的計算公式:
給定的字符串叫做模式串P。i表示next函數的參數,其值是從1到n。而k則表示一種情況下的next函數值。P表示其中的某個字符,下標從1開始。看等式左右對應的字符是否相等。
i=0時,next[0]=-1;
i=1時,k的取值為(0,1)的開區間,所以整數k是不存在的,那就是第三種情況,next[1]=0;
i=2時,k的取值為(0,2)的開區間,k從最大的開始取值,然后帶入含p的式子中驗證等式是否成立,不成立k取第二大的值。現在是k=1,將k導入p的式中得,P0=P1,即“A”=“B”,顯然不成立,舍去。k再取值就超出范圍了,所以next[2]不屬於第二種情況,那就是第三種了,即next[2]=0;
i=3時,k的取值為(0,3)的開區間,先取k=2,將k導入p的式子中得,P0P1=P1P2,不成立。 再取k=1,得P0=P2,不成立。那就是第三種了,即next[3]=0;
i=4時,k的取值為(0,4)的開區間,先取k=3,將k導入p的式子中得,P0P1P2=P1P2P3,不成立。 再取k=2,得P0P1=P2P3,不成立。 再取k=1,得P0=P3,不成立。所以next[4]=0;
i=5時,k的取值為(1,5)的開區間,先取k=4,將k導入p的式子中得,P0P1P2P3=P1P2P3P4,不成立。 取k=3,得P0P1P2=P2P3P4,不成立。再取k=2,將k導入p的式子中得,P0P1=P3P4,不成立。再取k=1,將k導入p的式子中得,P0=P4,成立。那就是第二種了 ,所以next[5]=1;
i=6時,k的取值為(1,6)的開區間,先取k=5,將k導入p的式子中得,P0P1P2P3P4=P1P2P3P4P5,不成立。 取k=4,得P0P1P2P3=P2P3P4P5,不成立。再取k=3,將k導入p的式子中得,P0P1P2=P3P4P5,不成立。再取k=2,將k導入p的式子中得,P0P1=P4P5,成立。那就是第二種了 ,所以next[6]=2;
即可得下表
其實,在計算的時候就會發現,這也是和之前說得計算特征數K值是一樣的思路,進而也得出Next[i]數組={-1,0,0,0,0,1,2},所以當熟悉的時候就可以很快求出模式字符串的Next數組(字符串的特征向量)。
下面再以另外一例子來該next數組還可進一步優化。
目標字符串T為:abacaabaccabacabaa
模式字符串P為:abacab
next向量根據上面的方法可以求出如下表:
在上圖可以知道,當進行第6次比較的時候,發現此刻是失配的,按照之前的“移動位數 = 失配字符所在的位置i - 對應特征數K”可以得出5-1=4,即右移4位,再次進行第7次比較,其他如此類推進行比較。
這里的確是KMP算法的思想,已經利用模式字符串的特征來跳過不必要的比較,但是細心的可以發現,在第6次比較的時候,目標字符串T中的a的確不等於模式字符串P的j=5位置的b,同時模式字符串P的j=5位置的b,我們可以根據已知的模式字符串P特征,得出j=1位置的b等於j=5位置的b,即可知道j=1位置的b不會等於目標字符串T中的a,當右移4位再次比較,已經沒有必要,此刻應該再右移進行比較,如下圖。
當 i = 0時,令next[i] = -1
其實,若pk = pi ,則有pk 不等於Tj;此時應再右移,使pnext[k]與 Tj 比較,故
第2步可進一步優化為:
- if (pk==pi)
- next[i] = next[k];
- else next[i] = k;
可以得
簡單說明優化Next求法:
i = 0時,依然是-1,而i=1時候,k為0,代入PK得PK=a,而Pi=b,顯然是不等
同理即求出其他關系,在這里說明一下,當PK vs Pi 的關系不等的時候,優化后的K值還是優化前的K值,而PK vs Pi 的關系相等,即優化后的K值為PK對應的k值。
計算字符串特征向量(優化)代碼:
- /*
- 計算字符串特征向量(優化版)
- */
- int *findNext(String P){
- int i = 0;
- int k = -1;
- int m = P.length(); //m為字符串P的長度
- assert(m>0); // 若m=0,退出
- int * next = new int [m]; // 動態存儲區開辟整數數組
- assert (next != 0); //若開辟存儲區域失敗,退出
- next[0]=-1;
- while(i<m){ //計算i=1...m-1的next值
- while (k>=0&&P[i]!=P[k])
- k-next[k];
- i++;
- k++;
- if(i==m) break;
- if(P[i]==P[k])
- next[i]==next[k]; //P[i]和P[k]相等,優化
- else
- next[i]=k; //不需要優化,就是位置i的首尾子串的長度
- }
- return next;
- }
到目前為止,整個KMP算法的思路已經很清楚。
KMP算法代碼:
- /*
- KMP模式匹配算法的實現
- */
- int KMPStrMatching(const String &T,const String &P,int *N){
- int i=0; //模式的下標變量
- int j=0; //目標的下標變量
- int pLen=P.length(); //模式的長度
- int tLen=T.length(); //目標的長度
- if (tLen<pLen) //如果目標比模式短,匹配無法成功
- return(-1);
- while (i<pLen&&j<tLen){ //反復比較對應字符來開始匹配
- if(i==-1||T[j]==P[i])
- i++,j++;
- else
- i=N[i];
- }
- if (i>plen)
- return (j-plen+1);
- else
- return (-1);
- }
kmp的應用優勢:
①高效,O(m+n)的線性最壞時間復雜度;
②無需回朔訪問待匹配字符串S.
總結:
小弟剛剛開始學數據結構與算法,可能文章某些地方有所欠缺與不足,請忘原諒並多加指點,同時本文參考了阮一峰,崔成龍和waytofall等的博文,同時在此基礎上有所修改,總體思路還是一樣,為了表示尊重,已經在最后列出原博文地址。最后感悟的是,站在大神的肩膀上看得更遠。
參考資料:
[1]張銘 王騰蛟 趙海燕 數據結構與算法 高等教育出版社
[2] 阮一峰 http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
[3] 崔成龍 http://blog.csdn.net/xiaoxian8023/article/details/8134292
[4] waytofall http://www.cnblogs.com/waytofall/archive/2012/10/27/2742163.html















