算法技巧总结(三)KMP

一、kmp算法的概念

  • 暴力解决法(朴素匹配法):其实就是从主串str1 和子串str2的第一个字符开始,将两字符串的字符一一比对,如果出现某个字符不匹配,主串回溯到第二个字符,子串回溯到第一个字符再进行一一比对。如果出现某个字符不匹配,主串回溯到第三个字符,子串回溯到第一个字符再进行一一比对…一直到子串字符全部匹配成功。

  • 这种算法在最好情况下时间复杂度为O(n)。即子串的n个字符正好等于主串的前n个字符,而最坏的情况下时间复杂度为O(m*n)。相比而言这种算法空间复杂度为O(1),即不消耗空间而消耗时间。

  • 之所以暴力解决法匹配字符串这么慢是因为若遇到有一个字符串不匹配时,它的回溯步骤太多了需要回到最起点。

  • 因此得到一个结论就是尽可能地减少回溯步骤,那咋操作呢???其实也就是kmp的主要思想 以空间换时间

string str1="ababadababacam";
string str2="ababaca";

如该图:当字符’d’与’c’不匹配,我们保持主串的指向不变,
主串依然指向’d’,而把子串进行回溯,让’d’与子串中’c’之前的字符再进行比对。如果字符匹配,则主串和子串字符同时右移。那子串应该回溯到那个位置呢???我们先放一下继续往下看!

二、最长相等前缀和后缀

  • 什么是字符串的最长相等前缀和后缀呢???

     abcjkdabc,那么这个数组的最长前缀和最长后缀相同必然是abc。 
     cbcbc,最长前缀和最长后缀相同是cbc。
     abcbc,最长前缀和最长后缀相同是不存在的
    

好了,我们现在懂得怎么求了字符创的最长相等前后缀了,那我们接着往下看。

  • 现在我们用图片来讲解最长相等的前后缀的实际应用

第一个长条代表主串,第二个长条代表子串。红色部分代表两串中已匹配的部分,绿色和蓝色部分分别代表主串和子串中不匹配的字符。
再具体一些:这个图代表主串"ababadababacam"和子串"ababaca"。
在这里插入图片描述

在这里插入图片描述
匹配到一定长度时发现有不匹配的地方,根据之前的说法也就是kmp的思想尽可能的减少回溯步骤,那应该回溯到那个位置呢?这时候最长相等前后缀就开始起作用了!因为红色的部分处有可能会有最长相等前后缀字符的,接着往下看!

在这里插入图片描述
在这里插入图片描述

灰色部分就是红色部分字符串的最长相等前后缀,而紫色部分就是最长相等前后缀相同的地方,我们子串移动的结果就是让子串的最长相等前缀和主串最长相等后缀对齐。

在这里插入图片描述
在这里插入图片描述
这一步弄懂了,KMP算法的也就差不多掌握了。接下来的流程就是一个循环过程了。事实上,子字符串中都有可能有最长相等前后缀,而且最长相等前后缀的长度是我们移位的关键,所以我们单独用一个next数组存储子串的最长相等前后缀的长度。而且next数组的数值只与子串本身有关。

三、next创建及代码讲解

  • 所以next[i]=k,含义是:下标为i的字符前的字符串中含有最长相等前后缀的长度为k。

我们可以算出,子串str2= "ababaca"的next数组长度为7及next[0]~next[6];

  • str2每次循环遍历的的字符串为a,ab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后缀的长度。由于a,ab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后缀是“”,“”,“a”,“ab”,“aba”,“”,“a”,所以next数组的值是[-1,-1,0,1,2,-1,0],这里-1表示不存在,0表示存在长度为1,2表示存在长度为3。
    从图中可知str1[5]!=str2[5],即出现了字符不匹配现象

在这里插入图片描述

  • 这时我们该如何移动呢,也就是让str1[5]与str2[5]前面字符串的最长相等前缀后一个字符再比较,而该字符的位置就是str2中的那个位置呢?很明显这里str2的位置是2,就是不匹配的字符前的字符串最长相等前后缀的长度。
    在这里插入图片描述

也是不匹配的字符处的next数组next[5]应该保存的值,也是子串回溯后应该对应的字符的下标。 所以str2那个位置就是next[5-1]=2,为什么这样是next[5-1]=2呢?因为我们数组元素中:-1表示不存在,0表示存在长度为1,2表示存在长度为3。接下来就是比对是str1[5]和str2[next[5-1]+1]的字符。这里也是最奇妙的地方,也是为什么KMP算法的代码可以那么简洁优雅的关键。

  • 接下来结合next数组创建代码讲解!
class KMP{
public:
    void calNext(string str,vector<int>&next){
        int k=-1;
        for(int i=1;i<str.size();i++){
            while(k>-1&&str[k+1]!=str[i])	k=next[k];//回溯
            //如果下一个不同,那么k就变成next[k],注意next[k]是小于k的,无论k取任何值。
            if(str[k+1]==str[i])	k=k+1;
            next.push_back(k);
            //这个是把算的k的值(就是相同的最大前缀和最大后缀长)赋给next[q]
        }
    }
private:
    vector<int>next{-1};
    //next[0]初始化为-1,-1表示不存在相同的最大前缀和最大后缀
 }

数组回溯问题

while(k>-1&&str[k+1]!=str[i])	k=next[k];//回溯
  • k为当前匹配最长相等的前后缀长度;

在这里插入图片描述
当str[k+1]等于str[i]时红色部分区域会扩展,相反当两者不相等时,即红色部分区域缩减。

  • 接下来已ababaca为例讲解回溯,灰色为当前最长前后缀长度
    在这里插入图片描述
    在这里插入图片描述
  • 当str[k+1]与str[i]相等时灰色区域就会增加,相反就会减少。那为什么k要k=next[k]这样赋值呢?而不是k–呢?
  • 其实也很简单呢,如果k–的话就不符合上面找最长前缀和最长后缀的原则,这里个人建议用笔画一下每次遍历的字符,k=next[k]回溯就是KMP最关键的地方。

四、kmp算法代码

  int kmp(string str1, string str2){ 
      calNext(str2,next);//创建next数组且收集数据
      int k=-1;
      for(int i=0;i<str1.size();i++){
          while(k>-1&&str1[i]!=str2[k+1])k=next[k];
          //这里跟next数组那边差不多
          if(str2[k+1]==str1[i])k=k+1;
          if(k==str2.size()-1)return i-str2.size()+1;
      }
      return -1;
  }

五、应用:实现 strStr()

  • 题目:
    在这里插入图片描述
  • 题解之一(kmp)
class Solution {
public:
    int strStr(string str1, string str2){         
       if(str1.size()==0&&str2.size()==0) return 0;
        if(str1.size()==0) return -1;
        if(str2.size()==0) return 0;
        calNext(str2,next);
        int k=-1;
        for(int i=0;i<str1.size();i++){
            while(k>-1&&str1[i]!=str2[k+1])k=next[k];
            if(str2[k+1]==str1[i])k=k+1;
            if(k==str2.size()-1)return i-str2.size()+1;
        }
        return -1;
    }
    void calNext(string str,vector<int>&next){
        int k=-1;
        for(int i=1;i<str.size();i++){
            while(k>-1&&str[k+1]!=str[i])k=next[k];
            if(str[k+1]==str[i])k=k+1;
            next.push_back(k);
        }
    }
private:
    vector<int>next{-1};
};

若该文章有出错请留言告知一下,谢谢~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值