总结——字符串模式匹配的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思路
想要理解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的情况。

 
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号