总结——字符串模式匹配的KMP算法

 KMP算法是用来解决字符串模式匹配问题,即在一个非定的目标串S中,确定模式串P是否是S的子串,并且给出子串的位置。

 

void BuildNext(char* p,int next[]){
  
    int pLen = strlen(p);  
    next[0] = -1;  
    int t = -1;  
    int j = 0;  
    while (j < pLen - 1){ 
        if (t == -1 || p[j] == p[t]){  
            ++j;  
            ++t;  
            next[j] = t;  
        }else{  
            t = next[t];  
        }  
    }
}  

int KMP(char* s, char* p){

    int i = 0,j = 0;  
    int sLen = strlen(s);  
    int pLen = strlen(p);  
    while (i < sLen && j < pLen){     
        if (j == -1 || s[i] == p[j]){  
            i++;  
            j++;  
        }else{  
            j = next[j];  
        }  
    }  
    if (j == pLen)  
        return i - j;  
    else  
        return -1;  
}  
KMP完整C代码

 

一、KMP思路

  想要理解KMP,就要先理解最朴素的暴力算法。KMP相当于对它的优化。

  暴力算法的复杂度是O(nm),即最坏情况下对每一个位置的主串数据遍历一遍模式串。其缺点在于,对主串数据重复遍历了很多遍,对模式串更是遍历了无数遍,尽管早就知道其中的数据了。

  KMP的优化在于提前处理一遍模式串O(m)后,只需对主串元素进行一次遍历O(n)。复杂度O(m+n).

  KMP算法的特点即:主串扫描不后退

 

二、主代码实现

int KmpSearch(char* s, char* p)  
{  
    int i = 0;  
    int j = 0;  
    int sLen = strlen(s);  
    int pLen = strlen(p);  
    while (i < sLen && j < pLen){  
        //①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++      
        if (j == -1 || s[i] == p[j]){  
            i++;  
            j++;  
        }else{  
            //②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]      
            //next[j]即为j所对应的next值        
            j = next[j];  
        }  
    }  
    if (j == pLen)  
        return i - j;  
    else  
        return -1;  
}  

  

举个例子,当匹配到如下位置时,

  S ababaababacb
  P ababacb
                     ^

此时,i=j=5,而S[i]!=P[j],所以令j=next[j]=3,i不变。

  S ababaababacb
  P     ababacb
                     ^

可以看到,为什么令j=next[j]=3呢?因为,模式串的前缀“aba”与主串[0..i-1]部分的后缀是相同的,也即模式串的前缀“aba”与主串[0..j-1]部分的后缀是相同的。但此时S[i]!=P[j],所以仍然令j=next[j]=1;

  S ababaababacb
  P         ababacb
                     ^
此时指向的两个字符仍不相同,j=next[j]=0;

  S ababaababacb
  P     ababacb
      ^

此时可以继续匹配。

但若是上一步的结果仍不匹配呢?

  S ababaababacb
  P     bbabacb
         ^

此时,这种边界情况需要我们特殊处理,我们设置哨兵next[0]=-1,令j=next[0]=-1。然后假设不论P[-1]遇到S[i]是否相等,都假设二者相等,i++;j++;则结果变为

  S ababaababacb
  P       bbabacb
           ^ 

可以看到,上述算法的关键在于如何构造next[].

next[j]存的是p[0:j-1]子串中最长相同前后缀的长度。特殊地,令next[0]=-1;

 

三、next[]表的构造

void GetNext(char* p,int next[])  
{  
    int pLen = strlen(p);  
    next[0] = -1;  
    int t = -1;  
    int j = 0;  
    while (j < pLen - 1){  
        //p[t]表示前缀,p[j]表示后缀  
        if (t == -1 || p[j] == p[t]){  
            ++j;  
            ++t;  
            next[j] = t;  
        }else{  
            t = next[t];  
        }  
    }
}  

而next[]的求法,实际上是对模式串自己进行模式匹配,方法和KMP类似,只不过从i=1开始。

即预处理t=-1的开始情况后,next[1]=0,从如下位置开始:

  P  ababacb
  P'   ababacb
        ^

当前位置不相等,所以P'指针回退。

  P  ababacb
  P'      ababacb
     ^

接着,两个指针继续扫描,j移动的同时,更新next[j==2]=t=0

  P  ababacb
  P'     ababacb
    ^

此时相同,则指针同时继续扫描,更新next[j==3]=t=1,

  P   ababacb
  P'      ababacb
       ^

然后继续,next[4]=2,next[5]=3

  P   ababacb
  P'      ababacb
        ^

此时,不匹配,令P'指针回退,由于此时随着P指针j的移动,next[]已经逐步建立,所以此时t回退到next[t]=next[3]=1;

  P   ababacb
  P'             ababacb
      ^

此时,不匹配,t继续回退

  P    ababacb
  P'               ababacb
      ^

到哨兵位置,与初始情况类似,并更新next[6]=0;

  P    ababacb
  P'      ababacb
        ^

此时 j=pLen-1,结束。

 

步骤:

  设模式串 P 指针为 j ,次模式串 P' 指针为 t 。假设 next[j] 已知,注意表示的是 0..j-1 的最长相同前后缀已知,不是 0..j    

  ①若 P[j] == P'[t] 则并进且更新 next 表 t++; j++; next[j]=t; //当前匹配,更新表

  若 P[j] != C[t] 则后缀指针回退(即前移)且不更新 next 表//当前不匹配,后退不更新

  ②重复上述步骤至扫描到 P 最后一个数据。

 

注:

  ① next[0]=-1;

  j 指向的是 P[0:j-1] 与 P'[0,t-1] 匹配,所以next[0]=-1相当于在-1处引入一个不存在的哨兵,简化代码统一理解。将特殊情况转化为普通情况进行处理。同样可以将其视为一种标记,即第一个元素都不匹配时,标记j<0,然后重新处理。

 

  ② 因为 next 存的是指正指向位置之前的子串的信息,P 中最后一个数据的信息实际上并未存进 next,因为如果最后一位相等则循环结束无需回退,而如果不相等,回退的位置与最后一位无关。

 

分析:

  看到网上有用数学归纳法来解释求解步骤。(数学归纳法有三个步骤,①初始条件成立②假设n=k情况成立③从n=k情况推导n=k+1的情况,若n=k+1的情况成立,则公式对自然数集成立。)其实感觉并不完全准确。这里用递推来说更准确一些。递推说的是,已知初始情况,则以后的任意n+1的情况均可以由n的情况推出。只是应用到这个问题上求解步骤略微有些不同,递推的初始条件其实是很难界定的,我们不妨事后再进行考虑,我们可以先假设f(n)已知,然后考虑如何从n的情况推导出n+1的情况。

 

posted @ 2015-11-06 12:27  Travelller  阅读(513)  评论(0)    收藏  举报