kmp
為了實現復雜度低的字符串匹配算法,將依次順序的掃描算法O(n*m)的復雜度降到O(n+m) 的算法就有了kmp(knut-Morris-Pratt算法)。
字符串匹配,簡單的來說就是在母串S中尋找是否含有模式串T,這種字符串匹配是計算機的基本任務之一。
kmp算法不易理解,網上有很多解釋,讀起來都很難以理解,看到了一個很好地總結http://kb.cnblogs.com/page/176818/ 現在真正的看懂理解了這種算法,下面在結合這篇博文的基礎上進一步講解一下基本kmp實現過程中的代碼如何理解,和kmp的優化如何實現。
首先簡單介紹一下博文中的對kmp的理解:
博文中講解的順序查找的算法和優化思想不再贅述,對博文中的部分匹配值得理解做詳細描述,引入next數組的概念,要理解
1. 正確理解前綴和后綴的意思,eg: ABCD的前綴分別是A, AB , ABC , ABCD
后綴分別是 D , CD , BCD , ABCD。注意,千萬不可以把后綴理解成從后往前讀的字符子串,這里明確一點:前綴一定是要包含當前字母前面的所有的字符,后綴一定要包含當前字符后面的所有的字符,這樣對於next數組后面的解釋才能正確理解其在整個算法中的作用。
2. 從next數組的定義上來理解:next[i]表示的是i位置前的串中,所有前綴和后綴中最長共有元素的長度,也可以說是共有元素長度中的最大值,引用博文中也有具體舉例。(注意:同引用博文不同的是這里我們為了方便代碼書寫,所有的串下標默認從0開始編號,所以請仔細理解公共長度,這樣方便與第二種next的用法上的理解對應)
在理解了前綴和后綴后進一步的解釋后,next數組為什么要這么定義,首先,簡單的理解就是i位置前的next[i]個字符和從開頭數的next[i]個字符是一樣的,那么匹配的時候如果是從i 處失配了,那么說明前面的所有部分都是已經匹配好的,那么現在如果前面的next[i]個字符和最后匹配好的字符是完全一樣的就可以直接將這next[i]個放到這后面next[i]個位置上,即從第next[i]個位置從新開始匹配,那么就不需要移動母串的指針,直接將子串滑動到第next[i]的位置。下面看一個例子
母串 ABCABCFGABABD
模式串 ABCABD
標紅地方失配了,而失配處前面的AB是已經匹配好的,根據前面的講解,模式串應該滑動到下面的位置
母串 ABCABCFGABABD
模式串 ABCABD
2. 有了這些知識我們來理解next數組的另一種理解,即next數組在算法中具體應用的理解:next[i]表示在i處失配話將
當失配的時候模式串要定位在模式串的第next[i]的位置上,這里要注意到上面提到的理解公共長度的部分,公共長度,由於數組標號是從0開始的所以失配位置要是想從新定位,看上面的例子可以理解應該是定位在相同部分的后一個位置,而后一個位置的下標剛好是公共長度(自己仔細思考,不再贅述)
那么,問題來了,怎樣求next呢?(大致思路就是如何求最大的前綴和后綴的共有元素長)
我們先來給出代碼,然后再一點點理解
1 int kmp(char* s , char* t) 2 { 3 int len1 , len2; 4 len1 = strlen(s); 5 len2 = strlen(t); 6 int i , j = 0 , tm = next[0] = -1; 7 //求next數組 8 while(j<len2-1){ 9 if(tm<0||t[j]==t[tm]) 10 next[++j] = ++tm; 11 else tm = next[tm]; 12 } 13 //匹配 14 for( i=j=0 ; i < len1 && j < len2 ; ) 15 { 16 if(j<0||s[i]==t[j]) i++,j++; 17 else j = next[j]; 18 } 19 if(j<len2) return 0;//如果沒有找到要返回0 20 return i-j; 21 }
可以看到kmp算法的代碼很短,我們先來分析求next數組的部分
1 //求next數組 2 while(j<len2-1){//依次求出模式串中每一個位置上的next值,循環j-2次即可,因為每次j是先++后處理的,相當於這個循環求出了下標從1到n-1位置的next值,下標為0的已經初始化時候定義過了 3 if(tm<0||t[j]==t[tm]) 4 next[++j] = ++tm; 5 else tm = next[tm]; 6 }
測試(可輸出)
1 #include<cstdio> 2 #include<cstring> 3 #include<string> 4 using namespace std; 5 int next[1500]; 6 int i , j = 0; 7 int tm = next[0] = -1; 8 int get_next(char* t) 9 { 10 int len = strlen(t); 11 while(j<len-1){ 12 if(tm<0||t[j]==t[tm])//tm<0是為了當找不到任何前綴等於后綴,而且不是字符串第一個字符的時候next值是0 13 next[++j] =++tm;//j和tm先加1再賦值,保證每次比較的字符串都不包含最后一位 14 else tm = next[tm];//如果這一位適配了,那么說明失配的字符前面的是已經匹配好的,然后要找失配位置的前面的字符串的next,即失配前的字符串中前綴等於后綴的長度 15 } 16 } 17 int main() 18 { 19 char s[10]; 20 scanf("%s",s); 21 get_next(s); 22 for(int i= 0 ;i < strlen(s);i++) 23 printf("%d ",next[i]); 24 puts(""); 25 return 0; 26 }
現在來分析一下求next主要代碼
if(tm<0||s[j]==s[tm]) {j++,tm++; next[j]==tm;}
else tm = next[tm];
舉例來說:其實可以將求next的過程看成是第二次的kmp匹配過程。
abcabfabcabg...
開始j定位到abcabfabcabg...
tm = -1,然后j和tm先++,然后就next[1] = 0——相應的對應b前面的字符串的公共前后綴長度為0,注意這里next[0]= -1是初始化了的;
然后比較abcabfabcabg...發現不匹配,然后讓tm = next[tm],這里不易理解這個語句的用途,下面一直分析,分析到
當要比較abcabfabcabg...發現不匹配,這時候tm = 5 ; j = 11;發現不匹配,由於next的連續性,所以從第一個字符開始到f的所有字符,和從g前面數相同數目的字符肯定是匹配好的,這里要仔細理解next數組的含義,即前綴和后綴的公共最大長度,所以這里找f前面的字符的next數組,這里表示的是f前面的
abcabfabcabg...由於g前面的和f前面的是已經匹配好的所以粉色的表示f的已匹配的前綴,即長度為next[5]的長度,肯定等於黃色的ab(根據next定義)也定於綠色的ab(由於g和f之前是匹配好的)那么就相當與是ab在f和g之前就不用再考慮了,下次比較的時候就是g和橘黃色的c開始比較了
那么問題自然來了,有人會問如果串時這樣的呢。。。。。注釋1;
abcabfabcaeg...那么我們要想其實在綠色的部分已經失配了,即當tm = 4 ; j = 10 ; 的時候就已經失配了,那么tm 要跳到next[4] = 1 ,指向黃色位置
然后下次是tm = 1和j = 10 比較,即每次都是tm 向前跳而j 不變,然后發現tm = 1和tm = 10仍然不一樣,所以跳到tm = next[tm] = 0 ,仍然失配,然后跳到tm = -1 ;
所以串在匹配的時候從abcabfabcabg...只要是一失配就向前跳一直跳到-1或者是滿足注釋1的地方,所以,只要是tm和j比較的時候一定是可以保證tm前面到串開始 和 從j前面的相同個數的字符是完全相同的。
現在就可以理解了next的數組了,如果不能理解,請再仔細閱讀一遍,仔細思考。
下面我們來說一下一個簡單的優化
假設在模式串的第i個位置失配,並且t[i] = t[next[i]], 那么令j= next[j]時依然失配,所以此時優化方法是令next[j] = next[next[j]].代碼如下
1 int kmp(char* s , char* t) 2 { 3 int len1 , len2; 4 len1 = strlen(s); 5 len2 = strlen(t); 6 int i , j = 0 , tm = next[0] = -1;//初始化為-1為了,當第一個都不匹配的 7 //求next數組 8 while(j<len2-1){//依次求出模式串中每一個位置上的next值,循環j-2次即可,因為每次j是先++后處理的,相當於這個循環求出了下標從1到n-1位置的next值,下標為0的已經初始化時候定義過了 9 if(tm<0||t[j]==t[tm]){ 10 ++j;++tm; 11 if(next[j]==next[tm]) tm = next[tm];//優化 12 next[j] = tm; 13 } 14 else tm = next[tm]; 15 } 16 //匹配 17 for( i=j=0 ; i < len1 && j < len2 ;) 18 { 19 if(j<0||s[i]==t[j]) i++,j++; 20 else j = next[j]; 21 } 22 if(j<len2) return 0;//如果沒有找到要返回0 23 return i-j; 24 }
1 //求next數組優化代碼 2 8 while(j<len2-1){//依次求出模式串中每一個位置上的next值,循環j-2次即可,因為每次j是先++后處理的,相當於這個循環求出了下標從1到n-1位置的next值,下標為0的已經初始化時候定義過了 3 9 if(tm<0||t[j]==t[tm]){ 4 10 ++j;++tm; 5 11 if(next[j]==next[tm]) tm = next[tm];//優化 6 12 next[j] = tm; 7 13 } 8 14 else tm = next[tm]; 9 15 } 10 16 //匹配
添加:
注意在next是c++庫里已經定理的名字,所以如果寫next會報ce,所以把開頭大寫比較好。