kmp算法


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,所以把開頭大寫比較好。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM