力扣刷题笔记:彻底搞懂KMP算法

一、KMP算法是什么?

简而言之,KMP算法是一种快速在一长串的字符串中找到与目标字符串完全相同的子字符串的方法。
例如:在"adaacffvg"中寻找’‘aacf’'这个字符串,就可以使用这个方法。

二、例题

Leet.28 实现strStr()
实现 strStr() 函数。

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。

说明:

当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。

示例 1:

输入:haystack = “hello”, needle = “ll”
输出:2
示例 2:

输入:haystack = “aaaaa”, needle = “bba”
输出:-1
示例 3:

输入:haystack = “”, needle = “”
输出:0

想要解出这道题目,我们可以使用双层for循环实现,但是其复杂度为O(m*n),也可以使用两个指针交替的方法进行比较,实现如下:

class Solution {
public:
    int strStr(string haystack, string needle) {
        if(needle.empty())
        {return 0;}
        int k = 0;
        for(int i=0,j=0;j<haystack.size()&&i<haystack.size();i++,j++)
        {
            while(j<haystack.size()&&k<needle.size()&&needle[k] == haystack[j])
            {
                k++;
                j++;
            }
            if(k == needle.size())
            {return i;}
            else
            {
                j = i;
                k = 0;  
            }
        }
        return -1;
    }
};

但是即使这种方法比双层for循环快一点,但如果不匹配依旧需要回溯到起始点,当模式串和文本串都比较长的情况下,也是比较费时的,所以我们需要一个复杂度更小的方法。

三、KMP算法

(1)前缀表

想了解KMP算法必须先了解前缀表,那么什么是前缀表呢,我们先要知道什么是前缀和后缀。
前缀:不包括尾字符在内的所有字串
例:字符串’‘aabaaf’‘的前缀有:“a”、“aa”、“aab”、“aaba”、“aabaa”
后缀:不包括首字符在内的所有字串
例:字符串’‘aabaaf’'的后缀有:“f”、“af”、“aaf”、“baaf”、“abaaf”

前缀表的值就是最长的相等前后缀
例:字符串’‘a’‘前缀长度为0,后缀长度为0,所以其最长相等前后缀的长度就是0;
字符串"aa",前缀和后缀都是"a",所以其最长相等前后缀的长度为1;
字符串"abaa",前缀有"a"、“ab”、“aba”,后缀有"a"、“aa”、“baa”。相等前后缀只有"a",所以最长相等前后缀的长度为1;
字符串"aabaab",前缀有"a"、“aa”、“aab”、“aaba”、“aabaa”,后缀有"b"、“ab”、“aab”、“baab”、“abaab”。最长相等前后缀是’‘aab’’,所以最长相等前后缀的长度为3;
一个字符串的前缀表的值是每个字符,字符前的字符串的最长相等前后缀的数组
例:字符串"aabaab"长度为6,那么我们可以创建一个数组记为next。
next [1] 的值对应第一个字符"a"前的最长公共前后缀,由于字符’‘a’‘前没有其他字符,所以next[1]=0;
next [2] 的值对应第二个字符"a"前的最长公共前后缀,第二个字符前只有一个字符"a",在上一步我们求过了,一个字符的字符串没有前缀和后缀,最长公共前后缀的值为0,所以next[2]=0;
next [3] 的值对应第三个字符"b"前的最长公共前后缀,第三个字符前的字符串为’‘aa’‘,最长公共前后缀的值为1,所以next[3]=1;
next [4] 的值对应第四个字符"a"前的最长公共前后缀,第四个字符前的字符串为"aab",最长公共前后缀的值为0,所以next[4]=0;
next [5] 的值对应第五个字符"a"前的最长公共前后缀,第五个字符前的字符串为"aaba",最长公共前后缀的值为1,所以next[5]=1;
next [6] 的值对应第六个字符"b"前的最长公共前后缀,第六个字符前的字符串为"aabaa",最长公共前后缀的值为2,所以next[6]=2;
在这里插入图片描述
tips: 计算相应字符位置在前缀表中的值时,是不包括自身的,是该字符前的字符串的最长相等前后缀

(2)KMP算法原理

当我们拥有了一个字符串的前缀表以后,那么我们就可以利用它来实现KMP算法了。

例:我们拥有一个文本串"abaaabaaffababba"和一个模式串"abaaf",我们想要在文本串中找到模式串相对应的位置。

我们立刻进行匹配:
在这里插入图片描述
发现第五位没有匹配上,f与a不相等,此时我们查看前缀表:
在这里插入图片描述
f在前缀表中对应的值是1,这就代表第一个字符和 f 前面一个字符是相等的,我们查看一下字符串,第一个是a,f 前一个字符也是a,说明前缀表没有问题。

由于 f 的前一个字符 a 已经和文本串匹配过,是配对的,而第一个字符又与其相等,所以说如果把第一个字符移动到 f 之前是完全能配对上的:
在这里插入图片描述
我们继续比较,现在字符b,占据了原来 f 的位置,但是还是与文本串不匹配,我们再次查看前缀表,发现 b 中对应的值是 0。b之前没有相同的字符,只能继续移动首字母匹配。
在这里插入图片描述
首字母移动一位后,完成了匹配。
原理解释的看不懂可以一下这个b站视频:KMP算法易懂版

(3)计算前缀表

想要用KMP算法解决问题,就免不了要计算前缀表,如何快速高效地计算前缀表就是一个问题,我们来看下面一段代码:

  void getnext(vector<int>& next,string needle)
    {
        int length = needle.size();
        int i = 1,j = 0;
        next[1] = 0;
        while(i<length)
        {
            if(j==0||needle[i-1]==needle[j-1])
            {
                next[++i] = ++j;
            }
            else
            {
                j = next[j];
            }
        }
    }

这个代码就是我用来计算前缀表next的完整代码,那么我们尝试去理解这个代码。
由于为了计算方便和与表值与字符串对应,我们将除了首字母以外的字符所对应的前缀表的值都加了1,如刚刚举例的字符串"abaaf"原本的前缀表next={0,0,0,1,1}变成了next = {0,1,1,2,2};

tips:由于字符串和数组从0开始计数,所以为了方便next数组从1开始计算。
     next数组的长度为字符串+1,next[0]不使用;
     下文提到的第几个数组在字符串中读取时都要-1,比如第一个字符就是char[0];

next数组的长度为字符串+1,next[0]不使用,next变为next={0,0,1,1,2,2};

代码第一步就是将next[1] = 0、next[2] = 1赋值给数组,因为前两个数字是永远也也不会变的,接下来就有点难理解了:next [ i ]的值其实取决于next[ i-1 ]的值,因为next [ i ]的最大值为next[ i-1 ]+1。

以字符串char = "abaaf"为例:

(1)next[1] = 0、next[2] = 1;

(2)在计算next[3]时,我们因为j = next[2] = 1,这就代表b之前没有相等前后缀,所以将 b 与字符串第1个数相比, 判断char[i-1]->b是否等于char[ j-1 ]->a,因为不相等所以,next[3]= next[1] + 1 = 1,j = next[3];

(3)next[4]:由于j = next[3] = 1,next[4]最长只能是2,所以只要比较第三个字符和第1个字符char[3-1]->a和char[j-1]->a就行,相等,所以next[4] = next[3] + 1 = 2, j = next[4] = 2;

(5)next[5]:由于j = next[4] = 2,next[5]最长只能是3,所以需要比较第4个字符和第2个字符char[4-1]->a和char[j-1]->b就行,不相等,j = next[j] = 1;所以next[5]最长只能是2,继续比较第四个字符与第一个字符char[4-1]->a和char[j-1]->a,相等,所以next[5] = next[1]+1 = 1;

在这里插入图片描述
若没看懂解释可以看这个视频4:12处开始。

四、求解例题代码

利用上述我们研究的KMP的方法,我们就可以解出例题中的代码了:

class Solution {
public:
    void getnext(vector<int>& next,string needle)
    {
        int length = needle.size();
        int i = 1,j = 0;
        next[1] = 0;
        while(i<length)
        {
            if(j==0||needle[i-1]==needle[j-1])
            {
                next[++i] = ++j;
            }
            else
            {
                j = next[j];
            }
        }
    }
    int strStr(string haystack, string needle) {
       if (needle.size() == 0) {
            return 0;
        }
       vector<int> next(needle.size()+1);
       getnext(next,needle);
       int j = 0;
       for(int i = 0;i<haystack.size();i++)
       {
           while(j > 0 && haystack[i] != needle[j])
           {
               j = next[j+1]-1;
           }
           if(haystack[i]==needle[j])
           {j++;}
           if(j == needle.size())
           {return i-needle.size()+1;}
       }
       return -1;
    }
};
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值