当前位置: 首页>>算法&结构>>正文


KMP算法的原理及实现【附C语言源码】(原创)

丹阳 算法&结构, 算法的力量 , 去评论

KMP算法是一种线性时间复杂的字符串匹配算法,它是对BF算法(Brute-Force,最基本的字符串匹配算法的)改进。

对于给的的原始串 \(S\) 和模式串 \(P\) ,需要从字符串 \(S\) 中找到字符串 \(P\) 出现的位置的索引。

BF算法的时间复杂度 \(O(strlen(S) * strlen(T))\) ,空间复杂度 \(O(1)\)

KMP算法的时间复杂度 \(O(strlen(S) + strlen(T))\) ,空间复杂度 \(O(strlen(T))\)

假设现在 \(S\) 串匹配到 \(i\) 位置, \(T\) 串匹配到 \(j\) 位置。那么总的来说,两种算法的主要区别在于失配的情况下,

对j的值做的处理:【注意,本文中的字符串下标都是从 \(0\) 开始计算】

BF算法中,如果当前字符匹配成功,即 \(s[i+j] == T[j]\) ,令 \(j++\) ,继续匹配下一个字符;如果失配,即 \(S[i + j] != T[j]\)

需要让 \(i++\) ,并且 \(j = 0\) , 即每次匹配失败的情况下,模式串T相对于原始串S向右移动了一位 。 (请结合下文源代码看这里的分析)

而KMP算法中,如果当前字符匹配成功,即 \(S[i]==T[j]\) ,令 \(i++,j++\) ,继续匹配下一个字符;如果如果失配,即S[i] != T[j],

需要保持i不变,并且让j = next[j],这里 \(next[j] <=j -1\) ,即模式串T相对于原始串S向右移动了至少1位(移动的实际位数 \(j - next[j] >=1 \) ),

同时移动之后,i之前的部分(即 \(S[i-j+1 \text{~} i]\)\(j=next[j]\) 之前的部分(即 \(T[0 \text{~} j-1]\) )仍然相等。显然,相对于BF算法来说,KMP移动更多的

位数,起到了一个加速的作用! ( 失配的特殊情形,令 \(j=next[j]\) 导致 \(j==0\) 的时候,需要将 \(i ++\) ,否则此时没有移动模式串 )。(请结合下

文源代码看这里的分析)

下面解释一下next数组的含义,这个也是KMP算法中比较不好理解的一点。

令原始串为: \(S[i]\) ,其中 \(0<=i<=n\) ;模式串为: \(T[j]\) ,其中 \(0<=j<=m\)

假设目前匹配到如下位置

\( S0,S1,S2,...,Si-j,Si-j+1...............,Si-1, Si, Si+1,....,Sn \)

\( T0,T1,...................,Tj-1, Tj, .......... \)

S和T的绿色部分匹配成功,恰好到Si和Tj的时候失配,如果要保持i不变,同时达到让模式串T相对于原始串S右移的话,可以

更新j的值,让 \(S_i\) 和新的 \(T_j\) 进行匹配,假设新的 \(j\)\(next[j]\) 表示,即让 \(S_i\)\(next[j]\) 匹配,显然新的 \(j\) 值要小于之前的 \(j\) 值,模式串才会是

右移的效果,也就是说应该有 \(next[j] <= j -1\) 。那新的 \(j\) 值也就是 \(next[j]\) 应该是多少呢?我们观察如下的匹配:

1) 如果模式串右移1位,即 \(next[j] = j - 1\) , 即让蓝色的 \(S_i\)\(T_{j-1}\) 匹配 (注:省略号为未匹配部分)

\(S_0,S_1,S_2,...,S_{i-j},\underline{S_{i-j+1}...............,S_{i-1}}, S_i, S_{i+1},....,S_n\)

\(T0,\underline{T_1,...................,T_{j-1}}, T_j, .......... \) (T的划线部分和S划线部分相等【1】)
\(\underline{T_0,T_1,................T_{j-2}},T_{j-1},.......\) (移动后的T的划线部分和S的划线部分相等【2】)

根据【1】【2】可以知道当 \(next[j] =j -1\) ,即模式串右移一位的时候,有 \(T[0 \text{~} j-2] == T[1 \text{~} j-1]\) 。而这两部分

敲好是字符串 \(T[0 \text{~} j-1]\) 的前缀和后缀,也就是说next[j]的值取决于模式串T中j前面部分的前缀和后缀相等部分的长度

 

2) 如果模式串右移2位,即 \(next[j] = j - 2\) , 即让蓝色的 \(S_i\)\(T_{j-2}\) 匹配

\(S_0,S_1,...,S_{i-j},S_{i-j+1},\underline{S_{i-j+2}...............,S_{i-1}}, S_i, S_{i+1},....,S_n\)

\(T_0,T_1,\underline{T_2,...................,T_{j-1}}, T_j, ..........\) (T的划线部分和S划线部分相等【3】)

\(\underline{T_0,T_1,.............,T_{j-3}},T_{j-2},..............\) (移动后的T的划线部分和S的划线部分相等【4】)

同样根据【3】【4】可以知道当 \(next[j] =j -2\) ,即模式串右移两位的时候,有 \(T[0 \text{~} j-3] == T[2 \text{~} j-1]\) 。而这两部分

也敲好是字符串 \(T[0 \text{~} j-1]\) 的前缀和后缀,也就是说 \(next[j]\) 的值取决于模式串T中j前面部分的前缀和后缀相等部分的长度

3) 依次类推,可以得到如下结论当发生失配的情况下, \(j\) 的新值 \(next[j]\) 取决于模式串中 \(T[0 \text{~} j-1]\) 中前缀和后缀相等部分的长度,

并且 \(next[j]\) 恰好等于这个最大长度。

上面给出了next数组的含义,下面给出求这个数组的具体算法。

1)显然有 \(next[0] = 0, next[1] = 0\)

2)观察【1】【2】可以看到如果 \(T[j]==T[j -1]\)\(T[j] == T[next[j]]\) 的情况下, \(j+1\) 前面字符串的前缀和后缀的相等部分长度增加了1

所以有 \(T[j]==T[next[j]]\) 的时候, \(next[j+1] = next[j ] + 1\) ;

同样观察【3】【4】也可以看到如果 \(T[j]==T[j-2]\) 亦即 \(T[j]==T[next[j]\) 的情况下, \(j+1\) 前面的字符串的前缀和后缀相等部分的长度增加了1,

所以也有 \(T[j]==T[next[j]]\) 的时候, \(next[j+1] = next[j] + 1\) ;

综合上面的规律有当 \(T[j] == T[next[j]]\) 的情况下 \(next[j+1]=next[j] + 1\) ;

3)\(T[j] != T[next[j]]\) 的情况 \(next[j+1]\) 又该等于多少呢?拿【1】【2】来说,如果此时 \(T[j] != T[j-1]\) ,可以移动【2】对应的串,

直到【1】中的Tj等于下面【2】中对应的字符,此时就找到了 \(j+1\) 的最大前后缀。注意,移动的时候同样可以用到已经计算出

\(next\) 数组的值。用伪代码表示就是:


k = next[j];

while(T[k] != T[j]) k = next[k];//如果不等,移动模式串

if(T[k] == T[j]) next[j + 1] = k + 1;

else next[j+1] = k;

最后给出BF算法和KMP算法(有四个版本)的源码如下。

BF:


#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int indexofsubstr(const char* str, const char* p)
{
         int lenstr = strlen(str);
         int lenp = strlen(p);
         int i, j;
         for(i = 0; i < lenstr; ++i)
         {
               j = 0;
              while( i + j < lenstr && j < lenp && str[i + j] == p[j]) j++;
               if(j == lenp) return i;
        }
        return -1;
}

int main(int argc, char** argv)
{
        const char* str = "abcdefghijklmmnx";
        const char* p = "mmn";
        printf("index:%d\n", indexofsubstr(str, p));
        system("pause");
        return 0;
}

KMP V1.0


/*
 * Author:puresky
 * Date: 2010/12/21

 * Purpose: KMP 1.0, a linear algorithm for searching pattern string in a given string!
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


//print a integer array
void pnt(int a[], int n)
{
     int i;
     for(i = 0; i < n; ++i) printf("%d ", a[i]);
     printf("\n");
}

//calculate the next array
void makenext(const char* ptrn, int *next)
{
     int len = strlen(ptrn);
     next[0] = 0;
     next[1] = 0;
     int i = 1;
     while(i < len - 1)
    {
            int j = next[i];

            while(j > 0 && ptrn[j] != ptrn[i]) j = next[j];

            if(ptrn[j] == ptrn[i])  next[i + 1] = j + 1;
            else next[i + 1] = j;

            i++;
      }
      //pnt(next, len);

}

//KMP
int indexofsubstr(const char* str, const char* ptrn)
{
        int lenstr = strlen(str);
        int lenptrn = strlen(ptrn);
        int next[1024]; //假设模式串的长度不超过1024
        makenext(ptrn, next);


        int i = 0, j = 0;
        while(i < lenstr && j < lenptrn)
       {
              if(str[i] == ptrn[j])
              {
                      i++,j++;
              }
             else
             {
                      j = next[j];
                      // if j euqals zero, increase i by 1. Otherwize, there may be a infinite loop!
                     if(j == 0) i++;

             }
             if(j == lenptrn) return i - j; //match successfully, return the index
       }
       return -1;
}

 int main(int argc, char** argv)
{
       const char* str = "abcdecdeabghijmnmnklamnmnaxabcabcabdxababacm";

       const char* p[6] = {"abcabd", "mnmna","mmx", "aaaaaaaabac", "cdecdea", "ababacm"};

       int i;
       for(i = 0; i < 6; ++i)
      {
             printf("S:%s\n", str);  

             printf("P:%s\n", p[i]);  
             printf("Index:%d\n", indexofsubstr(str, p[i])); 
             printf("*******************************\n"); 
      }
      system("pause");
      return 0;
}

运行结果如下:

11

KMP V2.0


//在KMP 1.0的基础上将next代码简化了一下,其思想为当模式串和模式串失配时,利用已经求出的next值将其中一个模式串

//移动适当的距离

#include <stdio.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>

#define MAX_PATTERN_LEN 1024

void print(int *a, int n)
{
       printf("next:");
       int i;
       for(i = 0; i < n; ++i) printf("%d ", a[i]);
       printf("\n");
}

void randstr(char a[], int n)
{
        int i;
        for(i = 0; i < n; i++)
              a[i] = 'a' + rand() % 3;

        a[n - 1] = '\0';
}

void getnext(const char* t, int *next)
{
        int len = strlen(t);
        int i,j;
        next[0] = 0;
        next[1] = 0;
        i = 1;
        j = 0;
       while(i < len)
       {
              if(t[i] == t[j])
              {
                      i++,j++;
                     next[i] = j;
              }
             else
             {
                     if(j == 0)
                     {
                           i++;
                           next[i] = 0;
                     }
                     j = next[j];
             }
        }
        print(next, len);
}

// ensure strlen(t) < MAX_PATTERN_LEN
int KMP_Index(const char* s, const char* t)
{
          int next[MAX_PATTERN_LEN];
          getnext(t, next);
 
           int lens = strlen(s);
           int lent = strlen(t);
           int i,j;
           i = 0;
           j = 0;
          while(i < lens && j < lent)
          {
                if(s[i] == t[j])
                {
                       i++, j++;
                }
               else
               {
                       if(j == 0) i++;
                       j = next[j];

                }
                if(j == lent) return i - j;
         }
         return -1;
}

int main(int argc, char** argv)
{
        srand(time(NULL));
        int times = 20;
        while(--times)
        {
             char s[1024];
             char t[1024];
             randstr(s, 100);
             randstr(t, 5);
             printf("S:%s\nT:%s\n", s, t);
             int r1 = KMP_Index(s, t);
             char* r2 = strstr(s, t);
  
             printf("%d:%s\n", r1, r2);
             if(r1 == -1 && r2 == NULL || r1 == r2 - s)  printf("check:TRUE ^_^\n");
             else printf("check:FALSE XXXX\n");
             printf("----------------------------------------------------\n");
       }
       system("pause");
        return 0;
}

 

KMP3.0


//KMP 2.0中,如果模式串为aaaaaaaa这种情形时,将会退化为BF算法,所以这里对next的计算做了小小的改进

#include <stdio.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>

#define MAX_PATTERN_LEN 1024

void print(int *a, int n)
{
 printf("next:");
 int i;
 for(i = 0; i < n; ++i) printf("%d ", a[i]);
 printf("\n");
}

void randstr(char a[], int n)
{
      int i;
      for(i = 0; i < n; i++)
             a[i] = 'a' + rand() % 3;

      a[n - 1] = '\0';
}

void getnext(const char* t, int *next)
{
      int len = strlen(t);
      int i,j;
      next[0] = 0;
      next[1] = 0;
      i = 1;
      j = 0;
      while(i < len)
      {
           if(t[i] == t[j])
           {
                 i++,j++;

                 //以下是改进的地方
                 if(t[i] == t[j])
                       next[i] = next[j];
                  else
                        next[i] = j;
            }
            else
            {
                   if(j == 0)
                   {
                         i++;
                         next[i] = 0;
                     }
                     j = next[j];
               }
        }
        print(next, len);
}

int KMP_Index(const char* s, const char* t)
{
      int next[MAX_PATTERN_LEN];
      getnext(t, next);
 
      int lens = strlen(s);
      int lent = strlen(t);
      int i,j;
      i = 0;
      j = 0;
      while(i < lens && j < lent)
      {
            if(s[i] == t[j])
            {
                    i++, j++;
            }
            else
            {
                   if(j == 0)   i++;
                    j = next[j];
              }
             if(j == lent) return i - j;
       }
       return -1;
}

int main(int argc, char** argv)
{
     srand(time(NULL));
     int times = 20;
     while(--times)
     {
            char s[1024];
            char t[1024];
            randstr(s, 100);
            randstr(t, 5);
            printf("S:%s\nT:%s\n", s, t);
            int r1 = KMP_Index(s, t);
            char* r2 = strstr(s, t);
  
            printf("%d:%s\n", r1, r2);
            if(r1 == -1 && r2 == NULL || r1 == r2 - s) printf("check:TRUE ^_^\n");
             else   printf("check:FALSE XXX\n");
             printf("----------------------------------------------------\n");
        }
        system("pause");
        return 0;
}

 

KMP4.0


//KMP算法的一种经典实现方法!

/*
 * Author:puresky
 * Date: 2010/12/22
 * Version: KMP 4.0,
 * Refer to:Handbook of Exact String-Matching Algorithms, Christian Charras & Thierry Lecroq
 */
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>

#define MAX_PATTERN_LEN 1024

//print the array
void print(int *a, int n)
{
         printf("next:");
         int i;
         for(i = 0; i < n; ++i) printf("%d ", a[i]);
         printf("\n");
}

//generate a random string
void randstr(char a[], int n)
{
         int i;
         for(i = 0; i < n; i++)
                a[i] = 'a' + rand() % 3; 

         a[n - 1] = '\0';
}

//preprocessing for KMP
void preKMP(const char* t, int next[], int len)
{
         int i,j;
         next[0] = -1; //Attention: next[0] equals -1 other than 0
         i = 0;
         j = -1;
         while(i < len)
         {
                 while(j > -1 && t[j] != t[i])
                          j = next[j];

                 i++, j++;
                 if(t[j] == t[i])
                         next[i] = next[j];
                 else
                        next[i] = j;
          }
          print(next, len);//output  the 'next' array
}

// KMP, ensure strlen(t) < MAX_PATTERN_LEN
int KMP(const char* s, const char* t)
{
          int next[MAX_PATTERN_LEN];
          int lens = strlen(s);
          int lent = strlen(t);
          preKMP(t, next, lent);
          int i,j;
          i = j = 0;
          while(i < lens)
         {
                  while(j > -1 && t[j] != s[i])
                          j = next[j];
                  i++, j++;
                  if(j >= lent) return i - j;
         }
         return -1;
}

int main(int argc, char** argv)
{
        srand(time(NULL));
        int times = 20;
        while(--times)
        {
                char s[1024];
                char t[1024];
                randstr(s, 100);
                randstr(t, 5);
                printf("S:%s\nT:%s\n", s, t);
                int r1 = KMP(s, t);
                char* r2 = strstr(s, t);
  
                printf("%d:%s\n", r1, r2);
                if(r1 == -1 && r2 == NULL || r1 == r2 - s) printf("check:TRUE ^_^\n");
               else printf("check:FALSE XXX\n");
               printf("----------------------------------------------------\n");
       }
       system("pause");
       return 0;
}
本文由《纯净的天空》出品。文章地址: https://vimsky.com/article/80.html,未经允许,请勿转载。