[KMP Substring Searh(Java)]KMP算法 个人学习笔记

KMP算法的个人学习笔记

关于KMP算法

KMP算法简单来说是用来解决字符串匹配问题的算法,名字的由来是提出这个算法的三个人名字的首字母。它的核心思想是利用上一次匹配失败后的信息,来减少下一次匹配的工作量,达到更快的匹配速度。
初次见到这个问题源于Leetcode的问题28,一道比较简单的题目,使用常规手段,将源字符串(长度为m)的每个字符逐一作为起点,与待匹配的字符串(长度为n)比较,就能得到答案。时间复杂度为O(m*n)。
使用KMP算法之后,时间复杂度可以达到O(m+n),也就是说,当m很大的时候,KMP具有更好的时间复杂度。

算法思路

中间一些步骤的实现思路不一定完全一样,我理解比较深刻的是在油管上看的一个视频,所以我的思路和做法会跟着视频的来,实际只要逻辑正确都可以完成KMP。

首先思考一下暴力破解的弊端在哪里

我们尝试匹配源字符串 src = abcababcabg 与模式串 ptr = abcabg,暴力破解的想法是,我们使用一个指针来标记当前遍历到的位置,如果该位置上,src==ptr,那么我们推进指针去考虑下一位置。以此类推,我们省略中间的步骤,直到发生矛盾;
暴力破解1
当我们的指针推进到索引为5的位置时,a != g
(本来图片是有索引的,但是因为做的时候没细心,导致索引中间有一个数字错掉了,我又不想从头来一遍,所以干脆把索引✄了,然后剪完我又发现上面还有个箭头也不见了……)
暴力破解2
这时候暴力破解的思路将会把指针重置到模式串开始的时候,推进模式串,从源字符串的索引1的位置继续比较;
暴力破解3
以此类推,直到索引3的位置下,a == a,我们又可以继续推进指针了,直到我们找到了匹配项,即指针走到了模式串末尾,或者源字符串中,指针后面的长度小于模式串,也就不可能继续匹配了。
暴力破解4

接着可以思考问题出在哪里了

当暴力破解法推进到上图这一步的时候,我们其实可以发现,我们当前比较的a和下一个位置将比较的b,其实必然是和模式串匹配的;
暴力破解中的问题1
其原因是,模式串中,矛盾点g前面的部分内,有一个子串ab,既是这部分的前缀,又是这部分的后缀(这个表述是我个人认为比较容易接受的),当我们的遍历进行到矛盾点时,其前面必然已经匹配过a和b两项;
暴力破解中的问题2
从而我们可以确定,模式串与源字符串在矛盾点前的两项必定能一一匹配,故我们其实无需将指针回退到模式串的开头,而只需要匹配矛盾点a与模式串中的c是否相匹配。
暴力破解中的问题3

这一点描述为:

对于任意一个模式串中的字符 T,我们可以找到一个数k,该字符串的前k个元素与T前面的k个元素一一对应。若k有多个,则取最大的一个。
这段话是看到的,忘了来自哪里了

换言之,就是确定,我们下一次匹配的时候,矛盾点前面有多少个字符是必定匹配,不需要考虑的。

KMP该怎么做

接下来讨论KMP算法具体要怎么做。
首先我们前面提到了,我们需要找一个数字,来确定这个矛盾点前面有多少个字符是之前匹配过的。
所以,我们在KMP算法中,构建一个Next数组,“如果中断发生在某个位置,我们可以找到Next数组中,该位置的前一个元素,来确定我们有多少个字符在下一次匹配中,可以不考虑”
关于这一点,不同实现方法不太一样,但是都大同小异,等实际构建起数组来,这一点就会比较清晰了。

由前面的讨论我们可以确定这个数组的一些特性,比如,因为我们总是找中断点的前一个元素,所以Next数组的最后一个元素是没有意义的,你找不到它(这一点在编码的时候好像忘掉了);再比如,数组的首个元素必定是0
接下来就开始构建Next数组吧

接下来我们开始构建Next数组

根据KMP的思想,我们要做的实际上还是匹配,去找模式串中某个字串内部的匹配。我们使用两个标记指针 ij,

i = 0
j = 1

比较模式串中 i 和 j 位置上的元素,不相等,则我们推进 j 指针,i 不动;
构建Next数组1
构建Next数组2
以此类推,直到 j 指针指向了索引为3的元素,此时 i 与 j 指向的元素相同,我们要做的是

Next[j] = i + 1 

然后同时推进指针 i 和 j;
构建Next数组3
同上,这次我们要做的同样是

Next[j] = i + 1

然后同时推进两个指针。
构建Next数组4
这时指针 i 和 j 下的元素不同了,我们这里需要做的是维持 j 指针,并回退 i 指针

i = Next[i - 1]

当然这个操作要在 i != 0 的前提下进行,这里的回退操作与Next数组在具体算法中的使用是一个道理,关于回退这个操作,我看了很多版本的解释,我对此的理解是:
我们可以把索引0到 j-1 这部分视为一个源字符串,0到i这部分视为一个模式串,那现在就是我们的匹配在模式串的索引为 i 的位置发生了中断,中断后我们需要回退指针i,其回退后的位置就是Next[i-1]的值,即:决定 i 之前有多少个字符是不再需要考虑的,在示例中这个值是0,所以回到了索引0的位置。
构建Next数组5
也就是下图的样子。其实因为之前提到过,这样的实现方式,中断之后要回退索引存储在前一个位置中,所以索引5的位置上的元素是没有意义的,如果没有后续的话,上一步其实可以直接退出。但是这样也许能更清楚的表达为什么需要回退到Next[i-1]的位置吧。
在这里插入图片描述

接下来可以回到源字符串上进行匹配了

如下图,我们使用两个指针来标记程序运行的进度

sp = 0
pp = 0

初始全部为0;
KMP1
然后我们开始按照暴力破解的方法执行,前面都一样,我们跳到发生中断的地方
KMP2
在这里我们同样要回退指针pp,不过这次的回退量由数组Next决定,中断位置的索引是5,所以我们找到前一个索引下,Next数组的值——2,将pp置为2,如下图,我们就完成了指针的回退,这个过程中sp指针不要动;
KMP3
到此,索引5仍然是不匹配的,所以我们进行类似的操作,这次将pp指针回退到了0位置,再继续进行匹配,最后就能完成匹配了,
程序结束时,pp的范围是 [0, ptr.length()]
sp的范围是 [0, str.length()]
且我们知道如果匹配是成功的,sp和pp必定分别位于两个字符串的末尾,所以返回sp-pp就是最终答案了。
KMP4

我的代码

首先是获取Next数组的代码,这部分是跟着视频的思路走的,所以有些地方逻辑可能不太符合正常的思维。另外关于前面提到的“最后一个元素的值无意义”这点,我记得我的代码好像没有修改,也就是说有时候最后一个元素还是会被赋值的。

    public static int[] getNextArr(String ptr) {
        int[] next = new int[ptr.length()];
        char[] match = ptr.toCharArray();
        next[0] = 0;
        int j = 1;
        int i = 0;
        while (j < ptr.length()) {
            //推进指针j直到末尾
            if (match[j] != match[i]) {
                //不匹配,回退指针,若指针本身在0位置,则不动
                i = i > 0 ? next[i - 1] : 0;
                if (match[j] == match[i]){
                    //回退后字符串匹配,这里做的事情和下面匹配的情况是一样的
                    next[j] = i + 1;
                    i++;
                }
            } else {
                //为Next[j]赋值i+1,推进指针i
                next[j] = i + 1;
                i++;
            }
            //推进指针j
            j++;
        }
        return next;
    }

接下来是进行匹配的部分:这里我的写法下只有一点要注意,就是判断pp是否到尾部的语句一定要放在匹配成功的情况下,不然肯定会出错(猪脑子,错过一次)。
上面的图解我个人感觉还是蛮详细的,对着图一步步走就能看懂了。

    public static int KMPSolution(String src, String ptr) {
        //ptr为空或长度小于src的情况做预处理
        if (ptr.length() == 0) return 0;
        if (ptr.length() > src.length()) return -1;

        int[] next = getNextArr(ptr);//转到子函数获取Next数组
        char[] sChars = src.toCharArray();
        char[] pChars = ptr.toCharArray();
        int sp = 0; //src pointer
        int pp = 0; //ptr pointer
        while (sp < src.length()) {

            if (sChars[sp] == pChars[pp]) {
                //当前字符匹配
                if (pp == ptr.length() - 1)
                    //若pp到尾,则函数终止,返回sp-pp为结果
                    return sp - pp;

                //pp未到尾,推进pp与sp
                sp++;
                pp++;
            } else {
                //当前字符不匹配
                if (pp == 0) sp++;//若pp在ptr的首部,则推进sp
                else pp = next[pp - 1];//否则回退指针pp到Next数组标记的位置
            }
        }
        return -1;
    }

这个代码最终在Leetcode的strStr问题下提交通过
提交
提交通过以后很多细节我都不想再修改了,如果有写的不好的地方请指正,另外图片全部是我自己用ProcessOn制作的,想要的可以直接拿走~~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值