大话数据结构---(四)KMP算法

看了很多KMP的相关资料,也看了一些博客,众说纷纭,说法其实大部分都没有错,只是理解角度不同。这也使得一些初学者由本来的一知半解,看了半天,到最后一头雾水。小编则是如此,折腾了一下午,才有话可说,这里就直奔主题吧。

1.KMP模式匹配算法原理

如果主串S=“abcdefgab”,我们要匹配的T=“abcdex”,那么如果用朴素算法的话,前5个字母,连个串完全相等,直到第6个字母,“f”与“x”不等,如图1所示。如果是朴素算法,那么它的下一步,将会是T的a对准S的b,再将每个字母依次比较。可仔细观察发现,对于要匹配的T,它的六个字母均不相同,当S和T的前五个字母一一对应,S的a与T的a相等,而T的a不与T中其它任何字母相等,所以必然不会和S中的b,c,d,e相等。效率较高的下一步操作,应该是将T的a与S的f进行比较,如图二所示。之所以是将T的a与S的f比较,是因为上一步中,我们得知S的第六个位置和T的第六个位置不等,而由于T的第一个位置与T的位置不等,这里并不能确定T的第一个位置和S的第六个位置的比较情况,而S的2,3,4,5位置则很容易确定不等。
在这里插入图片描述在这里插入图片描述那么这里有人就会问了,上面的例子是T中的所有字母均不相同,而如果T串后面也含有首字符“a”的字符怎么办呢?
我们来看下一个例子,如图3所示,假设S=“abcababca”,T=“abcabx”。对于开始的判断,前5个字符相等,第六个字符不等。根据刚才的经验,T的首字符“a”与T的第二位字符,第三位字符均不等,所以不需要做判断。因为T的第一位,第二位,分别与T的第四位,第五位相等。而在图3中,T的第四位,第五位已经和S串比较过了是相等的,因此可以判断,T的第一二位和S的第四五为相等,这两位也无需比较。也就是说,对于在子串中有与首字符相等的字符,也是可以省略一部分不要的判断步骤,直接跳到图4。
在这里插入图片描述在这里插入图片描述 在我们的朴素算法中,主串的i值是不断地回溯来完成的。而我们分析发现,这种回溯是不需要的—正所谓好马不吃回头草,我们的KMP模式匹配算法就是让这没必要的回溯不发生,既然i不回溯,也就是不可以变小,那么要考虑的变化就是T中的索引j了。比如图1到图2的过程,由于T=“abcdex”,当中没有任何重复的字符,所以j由6直接变成了1。而图3到图4,由于T=“abcabx”,前缀的“ab”与最后“x”之前串的后缀“ab”是相等的。因此j就由6变成了3。因此,我们可以得出规律,j值的多少取决于当前字符之前的串的前后缀的相似度。
我们把T串各个位置的j值的变化定义为一个数组next,那么next的长度就是T串的长度。于是我们可以得到下面的函数定义:
0,当j=1时
next[j]= Max{k|1<k<j,且’p1…p(k-1)’=‘p(j-k+1)…p(j-1)’}当此集合不为空时
1,其他情况

2.next数组值推导

在这里插入图片描述(1)当j=1时,next[1]=0;
(2)当j=2时,j由1到j-1就只有字符“a”,属于其它情况next[2]=1;
(3)当j=3时,同上next[3]=1;
(4)当j=4时,j由1到j-1的串是“aba”,前缀字符“a”与后缀字符“a”相等,next[4]=2;
(5)当j=5时,j由1到j-1的串是“abab”,前缀字符“ab”与后缀字符“ab”相等,next[5]=3;
(6)当j=6时,j由1到j-1的串是“ababa”,前缀字符“aba”与后缀字符“aba”相等,next[6]=4;
(7)当j=7时,j由1到j-1的串是“ababaa”,前缀字符“ab”与后缀字符“aa”并不相等,next[7]=2;
(8)当j=8时,j由1到j-1的串是“ababaaa”,只有字符“a”相等,next[8]=2;
(9)当j=9时,j由1到j-1的串是“ababaaab”,前缀字符“ab”与后缀字符“ab”相等,next[9]=3;
3.KMP模式匹配算法实现
计算出当前要匹配的串T的next数组

//通过计算返回子串T的next数组
void get_next(String T,int *next){
int i,j;
i=1;
j=0;
next[1]=0;
while(i<T[0])//此处T[0]表示串T的长度
{
   if(j==0||T[i]==T[j])//T[i]表示后缀的单个字符,T[j]表示前缀的单个字符
   {  ++i;
      ++j;
      next[i]=j;
   }else{
  j=next[j];//若字符不相同,则j值回溯
}
}
}

返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0

int Index_KMP(String S,String T,int pos)
{
  int i=pos;//i用于主串S当前位置下标值,若pos不为1,则从pos位置开始匹配
  int j=1;//j用于子串T中当前位置下标值
  int next[255];//定义next数组
  get_next(T,next);//对串T作分析,得到next数组
  while(i<=S[0]&&j<=T[0])//若i小于S的长度且j小于T的长度时,循环继续
  {
    if(j==0||S[i]==T[j])//两字母相等则继续,相对于朴素算法增加了j==0的判断
    {
      ++i;
      ++j;
    }else{
   j=next[j];//j退回合适的位置,i值不变
  }
 }
 if(j>T[0])
 return i-T[0];
 else
 return 0;
}

小结:对于get_next函数来说,若T的长度为m,因只涉及到简单的单循环,其时间复杂度为O(m),而由于i值不回溯,使得index_KMP算法效率得到了提高,while循环的时间复杂度为O(n)。因此,整个算法的时间复杂度为O(n+m)。相较于朴素匹配算法的O((n_m+1)*m)来说,是要好些。这里也需要强调,KMP算法仅当模式与主串之间存在许多“部分匹配”的情况下才体现出它的优势,否则两者差异并不明显。

发布了7 篇原创文章 · 获赞 7 · 访问量 347

猜你喜欢

转载自blog.csdn.net/Achenming1314/article/details/105078753