kmp算法

KMP

KMP算法可以用来确定模式串是否存在于主串中.在匹配的过程中,当模式串在某个位置上与主串发生失配的时候,不必回溯只需要按照模式串在该失配位置上的next函数值进行向右滑动,从而进行新的适配。

那么什么是模式串在失配位置上的next函数值

举个例子:

主串 :a c a b
模式串:a b
位置 :1 2

这里模式串在第二个位置与主串发生了失配,接下来就是将模式串右移一位继续比较,变为:

主串:a c a b
模式串: a b
位置: 1 2

经过移动后模式串从原来的第二个字符b与主串中的c进行比较变为了现在的第一个字符a与主串中的c进行比较
所以模式串中第二个位置(字符b)的next的函数值为1,即next(2) = 1;从这里我们也可以看到模式串在从第二个位置b与主串中的c发生失配后,模式串是经过右移才使得模式串中的a与主串中的c得以进行比较
接下来我们继续看一种情况1:

主串 :a c a b a a b a a a b c a
模式串:   a b a a b c a
位置:   1 2 3 4 5 6

我们可以看到模式串在第6个位置上与主串发生了失配,那么接下来模式串肯定是要右移的,我们可以直接将模式串右移到下面这种情况2的位置(至于为什么可以直接右移到指定位置我们在下面会解释)从而进行下一轮的适配:

主串 :a c a b a a b a a a b c a
模式串:     a b a a b c a
位置:     1 2 3 4 5 6

所以next[6] = 3.
这里我们有一个疑问,为什么可以往右移那么长的距离.

为解答这个问题这里我们有必要了解一下什么叫做前缀 什么叫做后缀 以及公共前后缀和公共最长前后缀.

我们以a b a a b来进行举例分别求它的前缀,后缀,公共前后缀,最长公共前后缀:
前缀: a ab aba abaa
后缀: b ab aab baab
公共前后缀: ab
最长公共前后缀: ab(因为公共前后缀只有一个所以ab既是公共前后缀也是最长公共前后缀)
在知道了上面的概念之后,我们可以看到上面情况1变为情况2是子串a b a a b从最开始的公共前缀a b移动到了后面的公共后缀a b,刚才提出的疑问"为什么可以往右移动那么长的距离"其实可以转化为“为什么子串a b a a b可以直接从最开始的公共前缀a b移动到公共后缀a b,从公共前缀直接移到公共后缀就不会错过模式串与主串完全匹配的机会吗?"这里我们可以做这样一个假设:假如上面情况1中子串a b a a b没有从公共前缀移动到公共后缀,而是在向右移动到第3个位置的时候模式串与主串发生了完全匹配.那么我们可以根据情况1中已有的部分匹配情况可以知道子串a b a a b中第3第4第5个字符与主串中第5 第6 第7 个位置的字符是相等的 现在假设模式串从模式串的第1个位置右移到第3个位置后模式串就与主串发生了完全的适配,说明子串的第1 第2 第3个字符与主串中对应的的第5 第6 第7个字符是相等的,与此同时模式串的第1 第2 与第3个字符 也一定与 模式串的 第3 第4 第5个位置的字符是一样的,既然如此,那子串a b a a b的最长公共前后缀的长度应该是3,对吧.显然这与我们的已知(子串 a b a a b的最长公共前后缀为 ab,最长的公共前后缀的长度为2)是矛盾的.所以我们可以将模式串右移,直接从情况1变为情况2.

通过上面的分析我们也知道next[6]不仅等于3也等于子串a b a a b的最长公共前后缀的长度(2) + 1.那反过来,假如以后我们知道了next[6] = 3 那么我们也可以知道子串 a b a a b的最长公共前后缀的长度为3 - 1 = 2,且最长公共前后缀为a b.以后我们在求模式串在某个位置的next的值的时候只需要找到对应的子串(比如上面所提到的模式串:a b a a b c a,第6个位置的对应的子串就为a b a a b)的最长公共前后缀的长度然后再加上一个1就可以了,对吧.

那么这个最长公共前后缀该怎么求呢?

现在情况2中模式串的第5个位置与主串发生了失配,我们假设已经知道了模式串的next[5] = 2,那由此我们也可以反推知道子串:a b a a的最长公共前后缀 = 2 - 1 = 1,而且这个公共前后缀是a
现在子串a b a a b 只是在子串a b a a后面多加了一个字符b 那么子串a b a a b 的第2个位置与第5个位置相同的话,那子串a b a a b的最长公共前后缀就为a b,最长公共前后缀的长度就为2.
恰好我们在求next[6]的时候是需要用到其对应子串:a b a a b的最长公共前后缀的长度的,所以我们可以推出next[6] = 2 + 1 = 3.
上面情况2是子串第next[5]个位置等于第5个位置的情况,那假如出现不相等的情况呢?比如还是上面情况2,在之前我们已经知道了next[6] = 3,依照之前求公共前后缀的求法我们来求next[7],所以这里需要对应子串a b a a b c的公共前后缀以及公共前后缀的长度,可是我们发现模式串第next[6]个位置的字符与第6个位置的字符是不一样的,接下来该怎么办呢?这时候我们可以将子串a b a a b c既看成是一个主串也可以看成是一个子串,这时候我们面对的是下面这种情况:

主串 :a b a a b c
模式串:   a b a a b c

当主串的第6个位置与模式串第next[6] (3)个位置不相等的时候,也意味着模式串中的第3个位置与主串发生了失配,此时模式串应当向右移动到next[3]的位置假设我们现在已经知道了next[3] = 1,我们需要将模式串从第3个位置右移直到第1个位置的字符a与主串中的第6个字符c进行比较,变为了下面这种情况:

主串 :a b a a b c
模式串:     a b a a b c

很显然a与c并不相等,接下来怎么办呢?我们继续递推,将右移后第1个位置的字符a与主串中的第6个字符是不相等的看成模式串a b a a b c 在第1个位置与主串a b a a b c 发生了失配,接下来模式串将从发生失配第一个位置右移到next[0](假设我们现在已经知道了next[1] = 0)个位置与主串中的c进行比较,变为下面这种情况:

主串 :a b a a b c
模式串:      a b a a b c

我们可以看到模式串的第0个位置是不存在字符与主串中的a(因为现在主串与模式串相等,所以也是模式串中的第6个字符)进行比较,至此,我们可以说子串a b a a b c是不存在公共前后缀的即公共前后缀长度为0,模式串a b a a b c 中第7个字符(这里我们假设模式串存在第七个字符)的next函数值为0 + 1.next[7] = 1.

求next函数值

我们这里规定next[1] = 0,此时由第一个字符所构成的子串的最长公共前后缀长度为0.通过上面的分析我们也可以知道:如果要求模式串中某个位置的next函数值只需要知道这个位置之前的的所有字符所构成的子串的最长公共前后缀长度然后加1即可.现在反过来想:我现在已经知道了模式串中前1个字符所组成的子串的最长公共前后缀的长度,那我是不是只需要+1就可以知道模式串中第2个位置的next函数值了,对吧.那现在第2个位置的字符以及next[2]也已经知道了,那现在就可以求出模式串中由前两个字符所构成的子串的最长公共前后缀的长度,然后加1就可以求出next[3],以此类推我们就可以求出模式串中所有位置所对应的next函数值了.

求next函数值的代码

void getNext(char s[],int *next){
  //标记当前位置
  int i = 1;
  //规定next[1] = 0;
  next[1] = 0;
  // j = next[i];
  int j = 0;
  //依次求每个位置的next函数值
  while (i<s[0])
  {
    //假如当前位置的字符与其对应的next的函数值相等
    if (s[i] == s[j] || j == 0)
    {
      //下个位置的的对应的next函数值为j+1
      next[++i] = ++j;
    }
    //假如当前位置的字符与其对应的next的函数值不相等 
    else
    {
      //令s[i]与s[next[j]]进行比较,一直递推直到出现相等或者j=0为止
      j = next[j];
    }
  }
}

int main() {
  //定义一个长度9的字符串s里面存有长度为8的模式串,首个字符存储字符串的长度
  char s[] = {9,'a','b','a','a','b','c','a','c'};
  //存储每个位置的next函数值
  int next[s[0]];
  //求next数组的值
  getNext(s,next);
  // 输出next数组
  for (int  i = 1; i < s[0];i++)
  {
    cout<<next[i]<<" ";
  }
  cout<<endl;
  return 0;
}

代码运行结果: 0 1 1 2 2 3 1 2

现在我们就可以利用求出的模式串的next函数值去求模式串在主串中的位置.

主串;a c a b a a b a a b c a c a a b c
子串:a b a a b c a c

void KMP(char* s, char* t, int* next){
  //主串中的比较指针
  int i = 1;
  //子串中的比较指针
  int j = 1;
  //逐个位置开始比较
  while (j < t[0] && i < s[0])
  {
    //主串与子串在比较指针指向的位置的字符相等,或者模式串指针指向0
    if (s[i] == t[j] || j == 0)
    {
      //比较指针后移
      i++;
      j++;
    }
    //如果不等
    else
    {
      //子串的比较指针回退到该位置的next函数值所指向的位置上
      j = next[j];
    }
  }
  //判断是否匹配
  if (j == 1)
  {
    cout<<"该模式串不是主串的子串"<<endl;
  }
  //输出匹配的位置
  else
  {
    //模式串长度
    int tLength = t[0] - 1;
    cout<<"模式串在主串第"<<i-tLength<<"个位置开始发生匹配"<<endl;
  }
}

参考资料

严蔚敏, 吴伟民. 数据结构[M]. 52. 清华大学出版社, 2007.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值