kmp算法

KMP算法

用处

kmp算法怎么用?现在先用一道题作为引入。

力扣28.找出字符串中第一个匹配项的下标
题目描述:给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。 示例 1:
输入:haystack = “sadbutsad”, needle = “sad” 输出:0 解释:“sad” 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。 示例 2: 输入:haystack = “leetcode”, needle =
“leeto” 输出:-1 解释:“leeto” 没有在 “leetcode” 中出现,所以返回 -1 。

遇到这种题目的时候,第一想法可能会先想到暴力破解,进行for循环遍历,遇到不对的然后从当前位置对needle字符串进行重新的配对。就进行如下的,只要不匹配了,needle字符串就往后。直到了完全匹配就再返回相对应的数组下标。
在这里插入图片描述

但其实会发现,采用了暴力破解的方法的话,时间复杂度是O(m×n)是很大。而且可以查看到对应得模式串如上图所示:sadsadb其实是匹配到最后一个字母b得时候才出现得不一样,而前面其实有出现sad的重复字母,完全没有必要进行重新的匹配,而kmp算法其实就是进行了这样子的方式进行问题的解决,进行字符串匹配的时候,如果遇到了不匹配的字符串,这个时候利用前面已经匹配了的字符串的信息内容,进行避免从头开始进行匹配。
在这里插入图片描述
这样子其实就会高效和快速,但是对于匹配到b的时候,是如何知道自己下一步已经跳转到什么地方进行匹配的呢?前面已经匹配了的字符串的信息内容怎么进行获取呢?其实就会需要一个前缀表进行存储来告知,当我的想要对应匹配的needle字符串出现了与haystack不匹配的字符串的时候需要进行如何的跳转,我就根据我的前缀表获取我前面匹配成功的子串的信息数据,告诉我需要进行如何的跳转。

前缀表

在知道前缀表存储的内容是什么之前需要进行知道什么是前缀,什么是后缀

前缀

前缀指的是:在一个字符串中,不包含最后一个字符的所有以第一个字符开头的连续子串。

后缀

后缀其实就正好和前缀是相反的,不包含第一个字符的所有以最后一个字符结尾的连续子串
如下图所示
在这里插入图片描述
而前缀表中是存储的就是当前子串中最长相等的前后缀的长度。而对于sadsad来说,她们最长相等的前后缀就是sad,然后长度为3。所以当上题中匹配的时候,出现了b不一样的时候,就会看前面d中其在整个字符串中是否有最长相等的前后缀,然后跳转到其相等的位置,这里是3,那就跳转到下标为3的位置,而这个时候,对于最开头的sad来说其实就不用再进行匹配了,因为之前第二次的sad已经帮他们匹配过是相等的了,而这个时候再进行匹配的话就速度会更加的快。
在这里插入图片描述
这个前缀表是needle的前缀表,而跟haystack是没有关系的,我们需要知道needle中是否有相应重复的部分可以让我们进行快速的匹配。
而对于上述中的sadsadb的前缀表其实就是如下:
在这里插入图片描述

具体代码

而如何求取前缀表的代码以及过程如下。然后在这里一定要先把前缀和后缀的概念弄清楚,不然其实在看的过程中会很乱以及奇怪的。

⭐前缀指的是:在一个字符串中,不包含最后一个字符的所有以第一个字符开头的连续子串。
⭐ 后缀其实就正好和前缀是相反的,不包含第一个字符的所有以最后一个字符结尾的连续子串

而对于前缀表一般是采用next数组进行构建的。而对于前缀表的构建步骤其实有下面的三步
①next数组的初始化
②进行前后缀不同的处理
②进行前后缀相同的处理。

next数组的初始化

对于数组的第0位,其实他的前后缀长度为0,因为就只有一个字符,那怎么说他的前缀和后缀呢?所以next[0]=0;

前后缀进行如何的标识

这里要引入两个变量,一个是标记前缀的结尾位置的变量,用j来表示,另外一个就是标记后缀的结尾位置的变量,用i来表示。而对于我们needle字符串的便利其实就可以采用后缀结尾位置的变量i来进行表示。

这里有一个点要注意就是什么是后缀的结尾位置以及前缀的结尾位置。以及为什么用i来遍历,而不是前缀的结尾位置来进行遍历。

这里是自己自己在分析后缀结尾的时候疑惑的地方,可能疑惑点比较特殊,大家可以不看这里引用的内容:
对于sasa来说,他的后缀是,a、sa、asa,而对于后缀的结尾就一直是a,对于后缀不要想着后缀就是从右往左来看,还是从左往右的,而对于自己求取后缀的子串来说,其结尾就是一直是他最末尾的位置。那为什么会用后缀结尾位置来作为遍历呢?

首先当我们进行一个字符串进行遍历的时候,还是用上面的sadsadb这个字符串,其遍历的过程是不是:sa、sad、sads、sadsa、sadsad、sadsadb。[注意这里的最后一个字符才是i所处的位置,而前面是已经遍历或者说是已经处理好的字符]。
而每次i的位置移动,是不是其实也就是我们后缀的结尾的位置移动,而移动中也就是我们字符串在不断的遍历移动了。其实列举出来就会理解了。
对于j来说,每次的遍历就是:s、sa、sad、sads、sadsa、sadsad就是这样子的遍历,所以才说j 是前缀的结尾的位置。另外关于j ,其实也就表示了最长相等前后缀的长度,因为j 所表示的位置是前缀的位置,而只要前后缀相等了,是不是就可以用前缀的长度或者后缀的长度表示呢?而对于后缀的长度,就需要对i进行复杂的处理,但是对于前缀来说其实就可以直接通过j 来知道了。
那对于她们的初始值来说,i的初始位置就是从1开始,而j是从0开始。而next[i]就是表示当前i的最长相等前后缀得长度。

前后缀不相同的情况处理

然后现在我们就进行处理前后缀匹配过程中出现不相同的情况应该如何进行处理。
首先是进行for循环得遍历

for(int i=1;i<needle.length();i++){

}

然后就使用j当前得位置和i当前位置的进行字符判断,如果不相等。则需要返回到前一个字符所拥有最长相等前后缀的位置,然后进行开始匹配。这样子就可以进行正确的判断是前缀和后缀相等的位置。因为我j 的位置不仅仅表示前缀结尾字符的位置也是表示着最长相等前后缀的位置。而且还有一点需要注意的是,这里要用while循环进行判断而不是if。因为我虽然让j 跳转到前一个位置的最长相等前后缀,但是却不能保证这个就是我当前i位置的子串的最长相等前后缀,所以还需要进行循环的遍历。直到说我当前的ji是相等的了。

while(needle.charAt(j)!=needle.charAt(i)){
j=next[j-1]}

而这里还有一点需要注意的就是,对于j 是不断的往前循环的,那么就需要有一个边界条件来进行描述的。不然当j 已经回退到0的位置的时候,还进行减一的话就越界了。所以这个循环中其实还有一个判断的。

while(j>0&&needle.charAt(j)!=needle.charAt(i)){
j=next[j-1]}

然后大概的一个流程图如下:
在这里插入图片描述

然后处理相等的前后缀

那其实就是判断,如果当前我的j 和我的i所对应的字符是相等的,那其实就是j 就继续往后移动,然后i也继续的往后移动,来记录我们最长相等的前后缀的长度。而对于当前的i来说,此时的j也表示了他最长相等的前后缀。还记得我前文中说的i表示的是后缀的结束位置也是最后一个字符的位置。所以代码就是

if (needle.charAt(j)==needle.charAt(i)) {
    j++; // i的增加在for循环里
}
next[i]=j;

那么当重复的完成上述的步骤,我们的前缀表其实也就进行创建出来了。完整的代码如下

    public void kmp(int[] next,String needle){
        int j=0;
        next[0]=0;
        for(int i=1;i<needle.length();i++){
            while(j>0&&needle.charAt(j)!=needle.charAt(i)){
                j=next[j-1];
            }
            if(needle.charAt(j)==needle.charAt(i)){
                j++;
            }
            next[i]=j;
        }
    }

然后获得了这个前缀表以后,就可以根据这个前缀表next数组中的数据,进行字符串的匹配问题了。只要遇到不同的,我就跳转到我们前面一个字符在前缀表中前长前后缀的长度位置进行重新的匹配,就可以避免了需要进行从头开始进行了。

最后

这是第一次自己学习完算法以后,把自己脑子理解的写下来,发现确实会很不一样,然后也让自己更加的清晰一些。可能自己有些地方表述奇怪或者啰嗦的也希望大家指出来,然后会尽快改正的。另外本文其实是自己看了代码随想录的视频以后进行自己复盘整理写出来的,大家可以去看看相应的视频我想可能也会理解的更透彻一些。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值