KMP字符串匹配算法

算法其实并不难,但是讲的费劲,看的也费劲……

**问题描述:**给两个字符串str和ptr,求str中是否有和ptr相同的子串并找出这个子串在str中的位置

这个问题的暴力算法很好想:从头开始一个字母一个字母地遍历str,然后对照ptr是否是从这个位置开始的子串。而KMP算法利用了前后缀的特殊性,简化了时间复杂度。
(从O(MN)变为O(M+N))

ps:算法思想可以参考:戳我看敲好理解的kmp算法鸭

(1)
首先介绍前缀后缀:如ABCDEF,前缀就是A,AB,ABC……而后缀就是F,EF,DEF……理解前后缀以后我们引入一个数组next,next中存放的就是到i这个位置(包括这个位置)之前前后缀相等且最长的情况。
如果不是很理解我们举个栗子 (划掉)例子 :在前面提到的网址中的两个字符串
str:BBCABCDABABABCDABCDABDE
ptr:---------------------- ABCDABD------------- (绝了,为什么不能用空 格对齐,只好这样了)
next[ptr]可以对应写出:
next[1]=0
next[2]=0
next[3]=0
next[4]=0
next[5]=1(相同前后缀:A)
next[6]=2(相同前后缀:AB)
next[7]=0

可以看出:ptr在D前面的一串都适配了,但是D和C不适配。
那么发现不匹配的时候,难道我们是将ptr位置直接++,然后继续比对吗?其实不必。

我们可以发现,ptr在D前面的序列ABCDAB中,前缀AB和后缀AB是相等的,即next[6]=2。重点来了!!那么我们可以直接把ptr从何前面移动到后面这个加粗的AB这里。因为我们已经从next数组中知道了在6这个位置,有一个长度为2的后缀等于前面长度为2的前缀。那么就说明直接将ptr移到这个位置,刚好可以匹配。

移动4=6-2
即公式:要移动的位置=已经匹配的数x-next[x]

可能会有人有疑惑,比如说:
str:ABxxxABxxxxxABC
ptr:ABxxxABxxxxxABD
next[14]=2-------------↑这儿
那么就把ptr:--------ABxxxABxxxxxABD
为什么ptr不直接跳过了中间的AB段而移动到了后面的AB段
其实可以举个反栗,如果中间那段AB开头的字符串和ptr匹配,那么就有更长的前后缀,就和之前定下的next[14]=2不符合了。

然后我们看怎么求next。直接贴上代码,这里的next和上面的next不一样。。这里的next求的是到这个位置的最长长度前后缀-1!记住要减1!因为后面代码从0开始计数,减一才能对应。

void kmp_next(char *s)
{
    int len = strlen(s); 
       next[0] = -1;
           int k = -1;
               for(int i = 1; i < len; i ++) 
               {
                       while(k > -1 && s[k+1] != s[i])
                        { 
                           k = next[k]; 
                        }
                        if(s[k+1] == s[i]) 
                        {
                                    k ++; 
                        } 
                        next[i] = k;
                }
 }

哇,看得我脑壳疼。
最难理解的应该是k=next[k]这一行。不方,其实这就是一个回退操作
指路:next算法
从求next开始那段,讲得很清楚了

全代码:

#include<iostream>
#include<string>
using namespace std;
#define maxn 101
int nextt[maxn];
void cal_next(string str)
{
 nextt[0]=-1;
 int k=-1;
 for(int i=1;i<str.length();i++)
 {
  while(k!=-1&&str[k+1]!=str[i])
  {
   k=nextt[k];
  }
  if(str[k+1]==str[i])
  {
   k++;
  }
  nextt[i]=k;
 }
}
int KMP(string str,string ptr)
{
 cal_next(ptr);
 int k=-1;
 for(int i=0;i<str.size();i++)
 {
  while(k>-1&&str[i]!=ptr[k+1])
   k=nextt[k];
  if(str[i]==ptr[k+1])
   k++;
  if(k==ptr.size()-1)
   return i-ptr.size()+1;
 }
 return -1;
}
int main()
{
 string s1,s2;
 cin>>s1;
 cin>>s2;
 int ans=KMP(s1,s2);
 cout<<ans<<endl;
}

后面放上一个kmp求循环节的模板:
定理:假设S的长度为len,则S存在最小循环节,循环节的长度L为len-next[len],子串为S[0…len-next[len]-1]。

(1)如果len可以被len - next[len]整除,则表明字符串S可以完全由循环节循环组成,循环周期T=len/L。

(2)如果不能,说明还需要再添加几个字母才能补全。需要补的个数是循环个数L-len%L=L-(len-L)%L=L-next[len]%L,L=len-next[len]。

代码:
(1)关于next的求取
nextt[i]:
nextt[0]=-1
nextt[1]=0;
这两个是固定的

i=1~n;存取的是当前这个位置(注意!不是从0开始!)有多少个最大相同前后缀
这样,虽然string是从下标0开始的,但是把它往后移了一位~
如果k=-1直接加一了

#include<iostream>
#include<string>
#define maxn 1000000+10
using namespace std;
int nextt[maxn];
void cal_next(string str)
{
 int n=str.size();
 int k=-1;
 nextt[0]=-1;
 int i=0;
 while(i<n)
 {
  while(k!=-1&&str[i]!=str[k])
  {
   k=nextt[k];
  }
  nextt[++i]=++k;
 }
}
int main()
{
 int T;
 int count=1;
 while(cin>>T&&T)
 {
  string str;
  cin>>str;
  cal_next(str);
  int index;
  int length;
  cout<<"Test case #"<<count<<endl;
  count++;
  for(int i=2;i<=T;i++)
  {
   int d=i-nextt[i];//循环节长度
   if(i>d&&i%d==0)//这个公式,推也可以,硬背也行
   {
    index=i;
    length=i/d;//循环周期
    cout<<index<<" "<<length<<endl;
   }
  }
  cout<<endl;
 }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值