KMP算法

感觉网上讲解的 KMP算法 看的我云里雾里的,而且还有很多公式,看着头大。

于是想着把自己理解的 KMP算法 写出来,以便之后忘记后能快速拾起。

我没有写数学理论,详解可以看LeetCode 28.实现 strStr() 官方解析

例题

LeetCode 28.实现 strStr()

简单来说,就是给两个 String 参数,主串 haystack, 模式串 needle。
现在,需要返回,如果在主串中存在子串,子串与模式串相等,那么返回子串首个字母在主串的位置。
如果没有这个子串,那么返回 -1.
全是小写英文字母。

空讲的感觉难以理解,那么还是举个栗子吧。

主串 haystack = “abaabecabaabcmmnr”
模式串 needle = “abaabcmmn”

关于模式串的前后缀匹配数组

这个数组是 KMP算法 的核心,说这个数组之前,需要了解一个概念。

前缀、后缀

前缀就是一个字符串从第一位开始向后,不到达最后一位的子串。对于模式串来说,“a”、“ab”、“aba”、…、“abaabcmm” 都是前缀,但是再多加一个 n 就不是前缀了。

后缀就是从字符串的非首位,必须到达最后一位的子串。对于模式串来说,“n”,“mn”,“mmn”、“cmmn”、…、“baabcmmn” 都是后缀。

匹配数组(next 数组)

前缀、后缀知道是什么了,那前后缀匹配就是前缀等于后缀呗。这个也好理解。

那么来计算一下,模式串从开始到结尾,遍历每个位置 i 时,从 0i 这个子串的 最大前后缀匹配字数 是多少。

这个数组我们暂时叫它 fl int[] fl = new int[needle.length()];,而且需要两个指针 i ji 来指向后缀位置,j 来指向前缀位置。

1、 首先遍历第一位,i0j0 ,此时的子串是 “a”,无法产生前缀和后缀,所以 fl[0] = 0;
我们可以在代码中直接跳过这一步,直接让 i 处于 1 位置。

2、 当 i = 1,j = 0。此时的子串是 “ab”。

在这里插入图片描述

蓝色方块代表 i 的当前指向,向上箭头是 j 的当前位置指向,红色方块代表 needle[j] != needle[i]
前缀 “a” 与 后缀 “b” 不匹配,此时的 fl[1]0;没有匹配串。

3、 当 i = 2,j = 0。此时的子串是 “aba”。

在这里插入图片描述

蓝色方块代表i的当前指向,向上箭头是j 的当前位置指向,绿色方块代表 needle[j] == needle[i]
前缀 “a” 与后缀 “a” 匹配,此时的fl[2]1.

此时,做一个设想,假如再多一位字母,这个字母恰好是 “b” ,也就是子串是 “abab”。
现在我已经知道,needle[j] == needle[i],那么,是不是只需要再判断 needle[j+1] == needle[i+1] 就可以了,前缀就不需要在从头一位位判断了。

在这里插入图片描述

好。。那我现在也就是说,当我遇到 needle[j] == needle[i],我就让j(前缀位置,箭头指向)向后移动一位,用于i 再拓展一个字符后的比较。

但是,还有一种情况,假如多的这个字母不是 “b”,是 “c”,子串就成了 “abac”。
在这里插入图片描述

那么,这也就意味着,从 2 位置开始的后缀在长度为 4 的这个字符串中,是不能和前缀进行匹配的。很明显,2位置开始的后缀 “ac” 不等于 同等字数的前缀 “ab”
通过这个图来看,我需要将后缀的开始位置向后调整一位,也就是用后缀 “c” 与前缀 “a” 去比较。也就是将j前调一位,然后用当前位置的ij 做比较。

再来看一种情况,看一个新的字符串
在这里插入图片描述
先不管前边怎么匹配的,就看现在的情况。
此时 i 指针指向的 "c" 不等于 j 指向的 "a"。难道我要将i 回调到3位置并且j回调到0位置吗?这个匹配数量还是短的,如果很长呢,那岂不是这么长的绿色匹配都做了无用功。而且i j 都修改容易混乱。

能不能有一个只调整一个指针,并且回调步长没那么长的办法?

仔细观察绿色子串,发现其中也有一个前后缀匹配串 "ac"

在这里插入图片描述
在这里插入图片描述
那我可以直接把前缀"ac"直接匹配在后缀"ac"上啊,然后我将j指针回调到2位置,再用needle[i]needle[j]进行比较。

而且整个过程只是 j 在调整位置,i一直没动,
回调的位置也不是从头开始的,而且回调到 fl[j-1]的位置,也就是 绿色子串的最大前后缀匹配字数。

此时还是不相等,那么继续我们这个步骤,
①将j回调到fl[j-1]的位置,注意j要大于0
②比较回调之后,needle[j]needle[i]是否相等

其实后边就不用再写了,到这里计算匹配数组的方法就可以出来了

int hlen = haystack.length(), nlen = needle.length();

// fl[n] 存储  模式串从0到n闭区间的子串 最大前后缀匹配子串的字数
int[] fl = new int[nlen];

for(int i = 1, j = 0 ; i < nlen ; ++i) {
    while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
        j = fl[j - 1];
    }
    if (needle.charAt(i) == needle.charAt(j)) {
        ++j;
    }
    fl[i] = j;
}

模式串匹配

主串、模式串匹配和计算匹配数组过程很像。

同样的,
i表示主串(上边字符串)的指针迭代位置(蓝色方块),
j表示模式串(下方字符串)迭代位置(向上的箭头),
绿色代表匹配上了,
红色表示不匹配,
黄色是模式串中匹配的前后缀。
在这里插入图片描述
在这里插入图片描述
如图,此时两个指针字符不匹配了,我需要将模式串的0位置去主串1位置进行比较吗?
不用。
因为fl存储的是最大的匹配串,所以直接用模式串匹配原串位置的后缀位置。即:
在这里插入图片描述
在这里插入图片描述
按照上边的步骤,
①当前位置不匹配,将j回调到fl[j-1]的位置,注意j要大于0
②比较回调之后,needle[j]needle[i]是否相等

之后的流程
在这里插入图片描述

for (int i = 0, j = 0 ; i < hlen ; ++i) {
    while (j > 0 && haystack.charAt(i) != needle.charAt(j)){
        j = fl[j - 1];
    }
    if(haystack.charAt(i) == needle.charAt(j)) {
        ++j;
    }
    if(j == nlen) return i - nlen + 1;
}

例题AC代码

class Solution {
    public int strStr(String haystack, String needle) {
        int hlen = haystack.length(), nlen = needle.length();
        // 模式串的长度是0,直接0位置就匹配上了
        if(nlen == 0) return 0;
        // 模式串的前后缀最大相同匹配数量
        int[] fl = new int[nlen];
        // i:遍历到的位置 也就是后缀最后一个位置,j:前缀的最后一个位置,
        for(int i = 1, j = 0 ; i < nlen ; ++i) {
            // j==0时,这个循环可以先跳过,一会在看
            // 好。此时的j大于0了,如果needle[j] == needle[i],去下边比较就好了,这里是用来缩短最长匹配串的。
            // 如果两者不等于,进入循环,,
            while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
                // 此时,你要思考一下,是不是我们匹配后缀的时候是从 1 位置开始的,到现在的 i 位置,也就是说needle[0:j-1] == needle[1:i-1]
                // 那么我现在就需要将后缀的开始位置向后移动了,常规是向后一位,比较 needle[0:j?] =? needle[2:i?]
                // 但是这样不是浪费时间吗,我们前边做了一个前缀后缀匹配字数长度(fl数组)了,那么我们为什么还需要将后缀向后移动一位呢
                // 直接从前后缀匹配的位置作为后缀的开始不好吗。也就是直接让 j = fl[j-1]
                j = fl[j - 1];
            }
            // 比较[后缀最后位置]与[前缀最后位置]是否匹配,当然,因为之前没比较过,所以最后位置就是最开始的位置
            // 如果相等,那么 j(前缀位置) 向后移动一位,,
            // 之后将会比较 needle[++j] == needle[i+1],因为此时 needle[j] == needle[i],所以下一位时,这个不用比较了
            if (needle.charAt(i) == needle.charAt(j)) {
                ++j;
            }
            // needle[0:i] 这个字符串前后缀匹配数量就是 0 到 j 的长度,
            // 因为上边对 j 加了一,也就是 j 现在就是长度,所以直接 fl[i] = j;
            fl[i] = j;
        }
        // 此时的i:主串的位置, j:模式串的位置
        for (int i = 0, j = 0 ; i < hlen ; ++i) {
            // 如果模式串当前j位置与主串i位置不相等了,那么移动模式串j到前后匹配的前缀后一位。说起来晦涩,还是看图比较明白
            while (j > 0 && haystack.charAt(i) != needle.charAt(j)){
                j = fl[j - 1];
            }
            // 如果相同,模式串j后移,之后for里边i会后移
            if(haystack.charAt(i) == needle.charAt(j)) {
                ++j;
            }
            // 匹配完成
            if(j == nlen) return i - nlen + 1;
        }
        // 都没匹配上
        return -1;
    }
}

算法还有改进空间,目前时间上不太充裕,先做个标记,之后来写

参考博文

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值