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]才不等,如图:

151750285.jpg

同时移动S和T,再继续比较:

151752752.jpg


151755447.jpg

151758769.jpg

此时,经过多次回溯才完成字符串的查找。代码见:

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。如图:

151801772.jpg

KMP算法的核心思想是利用已经得到的部分匹配信息减少后面匹配过程中重复的比较。看前面的例子, T[5]==’d’的模式函数值等于2(next[5]=2),这个2表示T[5]==’d’的前面有2个字符和开始的两个字符相同,且 T[5]==’d’不等于开始的两个字符之后的第三个字符(T[2]='c')。如图:

152630584.jpg

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算法相关描述总结,谢之~~。