字符串匹配算法

KMP是有名的字符串模式匹配算法,它根據模式串自身的特點在匹配的過程中出現失配時減少回溯來提高效率。KMP算法的時間複雜度爲 O(m+n)


1. 簡單的字符串查找

在分析KMP算法前,先看下簡單的匹配算法,其時間複雜度爲O(m*n)。

例如:在串 S=“abcabcabdabba”中查找T=“abcabd”(假設從下標0開始)。先是比較 S[0]和 T[0]是否相等,然後比較S[1]和T[1]是否相等…,發現一直比較到S[5]和T[5]纔不等,如圖:

同時移動S和T,再繼續比較:


此時,經過多次回溯才完成字符串的查找。代碼見:

int my_navie_str_find(const char * p,const char * s,int pos)
{

   if(p == NULL || s == NULL || pos < 0)

       throw std::new exception("Invalid Parameter!");

   int plen;
   int slen;
   plen = my_strlen(p);
   slen = my_strlen(s);

   int i, j;
   i = pos;
   j = 0;

   while(i<slen && j<plen){
       if(s[i] == p[j]){
           ++i;
           ++j;
       }else{
           i = i-j+1;/* 返回上一次成功匹配位置的下一個位置 */
           j = 0; /* 模式串從頭開始重新進行匹配 */
       }
   }
   if(j>=plen)
       return i-j;
   else
       return -1;
}


2. KMP查找算法

同樣在S=“abcabcabdabba”中查找T=“abcabd”,當第一次搜索到S[5]和T[5]不等後,S下標不是回溯到1,T下標也不是回溯到0,而是根據T中T[5]=='d'的模式函數值(next[5]=2,怎麼得到後面講),直接比較S[5]和T[2]是否相等。相等則S和T的下標同時增加...。最終在S中找到了T。如圖:

KMP算法的核心思想是利用已經得到的部分匹配信息減少後面匹配過程中重複的比較。看前面的例子, T[5]==’d’的模式函數值等於2(next[5]=2),這個2表示T[5]==’d’的前面有2個字符和開始的兩個字符相同,且 T[5]==’d’不等於開始的兩個字符之後的第三個字符(T[2]='c')。如圖:

KMP算法中模式值的定義:

(1) next[0]= -1

   任何串的第一個字符的模式值規定爲-1

(2) next[j]= -1

   a. T[j]=t[0] 且T[0]T[1]...T[k-1] != T[j-k]T[j-1+1]...T[j-1],即j前面的1~k個字符與T開頭的1~k個字符不等;

   b. T[j]=t[0] T[0]T[1]...T[k-1] == T[j-k]T[j-1+1]...T[j-1],即j前面的1~k個字符與T開頭的1~k個字符相等T[k]==T[j])(1k<j)

   如:T=abCabCad next[6]=-1,因T[3]=T[6]

(3) next[j]=k

   模式串T中下標爲j的字符,如果j的前面k個字符與開頭的k個字符相等,且T[j]!=T[k](1k<j),

T[0]T[1]T[2]…T[k-1] == T[j-k]T[j-k+1]T[j-k+2]…T[j-1] T[j] != T[k](1k<j)

(4) next[j]=0

   除(1)(2)(3)的其他情況。


next函數值含義:

設在字符串S中查找模式串T,若S[m]!=T[n],那麼,取T[n]的模式函數值next[n]繼續與S[m]比較。

1. next[n] = -1 表示S[m]T[0]間接比較過了,不相等,下一次比較 S[m+1]T[0]

2. next[n] = 0 表示比較過程中產生了不相等,下一次比較 S[m]T[0]

3. next[n] = k>0 k<n,表示S[m]的前k個字符與T中的開始k個字符已經間接比較相等了,下一次比較S[m]T[k]相等


示例T=“ababcaabc”的模式函數值。
next[0]= -1 根據 (1)
next[1]=0   根據 (4)
next[2]=-1  根據 (2)
next[3]=0   根據 (3) 雖然T[0]=T[2] 但 T[1]=T[3] 被劃入(4)
next[4]=2   根據 (3) T[0]T[1]=T[2]T[3]  且 T[2] !=T[4]
next[5]=-1  根據 (2)  
next[6]=1   根據 (3) T[0]=T[5]  且 T[1]!=T[6]
next[7]=0   根據 (3)  雖T[0]=T[6] 但 T[1]=T[7]  被劃入(4)
next[8]=2   根據 (3) T[0]T[1]=T[6]T[7]  且 T[2] !=T[8]


     0 1  2  3  4  5  6  7  8
T     a b  a  b  c  a  a  b  c
next -1 0 -1  0  2  -1 1  0  2

//next函數值計算

void my_kmp_get_next(const char * p, int plen, int * next)
{
   assert(next != NULL);

   int i, j;
   i = 0;
   j = -1;
   next[0] = -1;

   while(i<plen){
       /* 以i爲尾,  */
       if(j == -1 || p[j] == p[i]){
           ++i;
           ++j;
           if(p[i] != p[j])
               next[i] = j;
           else
               next[i] = next[j];
       }else
           j = next[j];
   }
#ifdef DEBUG
   for(i=0; i<plen; ++i)
       printf("n[%d]=%d\n", i, next[i]);
#endif
}

//kmp匹配算法
int my_kmp_str_find(const char * p,const char * s,int pos)
{

   assert(p != NULL && s != NULL && pos>=0);

   int plen;
   int slen;
   int i, j;
   plen = my_strlen(p);
   slen = my_strlen(s);
   i = pos;
   j = 0;

   //int next[plen] = {0};
   int *next;
   next = (int*)calloc(1, plen);
   my_kmp_get_next(p, plen, next);/* 計算失配時的跳轉數組 */

   while(i<slen && j<plen){
       /* 主串s[i]和模式串p[j]相等, s和p同時往後移動一位,繼續比較 */
       /* 出現失配, 模式串p的比較位置退到p[0]仍然不匹配, 同時移動s和p,繼續比較 */
       if(p[j] == s[i] || j == -1){
#ifdef DEBUG
           printf("p[%d]=%c s[%d]=%c\n", j, p[j], i, s[i]);
#endif
           ++i;
           ++j;
       }else{
#ifdef DEBUG
           printf("mismatch! p[%d]=%c s[%d]=%c\n", j, p[j], i, s[i]);
#endif
       /* 模式串p當前位置p[j] != s[i]出現失配, 主串不動,往前移動模式串p到某個位置與主串s[i]繼續比較 */
           j = next[j];
#ifdef DEBUG
           printf("next=%d\n", j);
#endif
       }
   }
   if(!next){
       free(next);
       next = NULL;
   }
   if(j >= plen)
       return i-j;
   else
       return -1;
}


注:本文是根據其他網友的KMP算法相關描述總結,謝之~~。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章