一口气掌握KMP算法

目的

网络上搜索KMP算法的文章和视频很多,五花八门。每个人的理解都有一定不同。next数组有从1开始的,也有从0开始的等等细节,会有点不知所以。导致有的人讲完后,感觉好像懂了,但又没懂。本人刚接触KMP算法也老是没弄清楚,一知半解,过一会就忘了。
最近终于捋清楚思路。所以写下这篇文章,主要以下两个目的。

  1. 从目前掌握的KMP知识,提供思路和想法。同时加深自己对KMP算法的理解和记忆。
  2. 让阅读者能够从这篇文章能有所收获。

这里分享下学习心得
首先我们不管KMP的代码是如何实现的,我们先要搞懂原理,原理懂了,实现有差异很正常。实现无非就是下标边界问题。这个根据个人喜好。自行选择。这里不是说代码不重要,代码可以反过来印证原理,加深对原理的理解。

什么是KMP算法

KMP算法是解决查找关键字的问题。给定一个字符串,要在这个文本串中查找特定的字符串,然后返回位置。比较专业的定义参考百度百科 KMP算法
有人可能会有疑问,为什么要有KMP算法,一句话效率高。

KMP算法原理

首先想下,如果没有KMP算法,要进行字符串查找要如何进行
举例
主串:ababcababcaaaaa
子串(模式串):abacababa
朴素的想法还是很容易想到。暴力求解直接就写两个循环。

  1. 主串指针mainIdx和子串指针patternIdx同时递进,然后对比。
  2. 遇到不相等的时候,mainIdx回退到下一个起始位置,patternIdx回退到第一个位置。

这样明显效率很低,算法复杂度为O(n*m)。代码实现略

KMP的算法的主要思路其实就是mainIdx指针不回退,然后patternIdx指针根据next数组来进行移动或者说跳转。(主串指针不动,子串指针移动)。这里的next数组是很关键的一个数组。
我们先从名字上理解next,有下一个的意思。所以这里记住next数组是干什么用的。

  1. 主串和字串进行逐个对比,
  2. 在遇到不相等的情况。主串指针不变。那子串的指针如何移动?就是通过查找next数组来进行移动。

引申出next数组如何获取

next数组生成原理

next数组生成只跟子串(模式串)有关。如何生成,原理看下图
蓝色和绿色表示最长相同的前后缀,一步一步直到最后一个字符。

在这里插入图片描述

为什么根据next值跳转,patternIdx的移动就是正确的?

数组有两个维度的信息,一个下标,还有一个是下标对应的值。

next数组每一个元素值的含义是如果该位置不相等,则patternIdx指针要跳转到该下表值指定的位置。

发现模式串不相等字符的前面字符串(matchedStr) 是已经匹配过的,
进一步分析这个字符串(matchedStr) 发现,这个字符串(matchedStr) 的最长公共前后缀是不是刚好就是我们对比过的。文字苍白,我们看图
如果理解下面这张图。你就知道为什么要求最长公共前后缀了。
在这里插入图片描述

红色的部分已经比较过,所以移动的时候,绿色部分不用再比较了,已经对比过了。(关注:鲁班曰)

KMP算法代码实战

next数组代码实战

大家看了上面的next生成的原理应该还是比较容易理解,那有人就会问看图理解很简单,关键是代码如何生成,代码不会像人类那样靠眼睛一个一个对比查找最长公共最后缀。闲话少说,代码如下:

public static int[] next(String pattern) {
        char[] patterns = pattern.toCharArray();

        // 默认都是0
        int[] next = new int[pattern.length()];

        // 匹配过程中 前后缀最长的值,既是长度,也是next数组下标,前缀指针
        // 这个字段要理解,是精髓,既是长度也是前缀索引下标
        int prefixIdx = 0;
        // 后缀下标字符串第二个开始进行计算,第一个默认是0
        int suffixIdx = 1;
        while (suffixIdx < pattern.length()) {
            // 如果相等
            if (patterns[prefixIdx] == patterns[suffixIdx]) {
                // 长度加1,前缀指针前移(同时也是长度加1)
                prefixIdx++;
                // 记录当前字串截止的最长公共前后缀长度prefixIdx;
                next[suffixIdx] = prefixIdx;
                // 后缀指针前移比较下一个
                suffixIdx++;

                // 这里需要注意,这三句代码的前后顺序不能调换

            } else {
                // 不相等的情况分两种情况
                if (prefixIdx == 0) {
                    // 如果不相等,记录当前字串截止的最长公共前后缀长度prefixIdx;
                    next[suffixIdx] = prefixIdx;
                    // 后缀指针前移下一个
                    suffixIdx++;

                } else {
                    // (关注:鲁班曰)
                    // 如果prefixIdx大于0,即长度不为零,
                    // 精髓所在,好好理解这句代码。
                    // 这句理解了,你就掌握了kmp
                    // 首先想想,如果你不这么写,你会这么写。
                    // 我的理解就是这里把模式串当主串,拿前缀当模式串。有点套娃的意思在里面。
                    // 想想next是干什么用的
                    // 主串和字串进行逐个对比,在遇到不相等的情况。主串指针不变。那子串的指针如何移动。
                    // 就是通过查找next数组来进行移动
                    // 这里是不是遇到不相等的情况了吗
                    // 前缀和后缀的字串patterns[prefixIdx] != patterns[suffixIdx]不相等,前缀(模式串)怎么移动
                    // 既prefixIdx怎么移动,不就是通过查找next数组吗?这里有点绕,多体会以下
                    // 前缀prefixIdx指针后退,后退的位置根据已知的next得知
                    // (根据已有的信息,获取下一个prefix的位置和长度,递推思想,根据已有next数组信息推出prefixIdx下一个值)
                    prefixIdx = next[prefixIdx - 1];
                }
            }
        }

        return next;
    }

KMP算法完整实战

步骤如下

  1. 生成next数组
  2. 主串指针mainIdx和子串指针patternIdx,一起向前进,逐个对比
  3. 遇到不相等的情况。mainIdx不动。patterIdx如何移动,next告诉你。
  4. 遇到不相等的情况。如果patterIdx=0。则mainIdx自己加1移动。
package com.ashin.dp;

import java.util.Arrays;

/**
 * 这里的next数组的含义,表示跳过几个字符,模式匹配的时候模式串的当前字符不匹配,则该不匹配字符外,剩余字符串的最长公共前后缀是多少,排除到自己本省
 *
 * @Author: Ash
 * @Date: 2020/9/4
 * @Description: com.ash
 * @Version: 1.0.0
 */
public class KMPExample {


    public static void main(String[] args) {
        String mainString = "ababaaababababbababadab";
        String pattern = "ababa";
        int[] next = next(pattern);

        System.out.println(Arrays.toString(next));
        
        int mainIdx = 0;
        int patternIdx = 0;
        while (mainIdx < mainString.length()) {
            if (mainString.charAt(mainIdx) == pattern.charAt(patternIdx)) {
                mainIdx++;
                patternIdx++;

            } else if (patternIdx > 0) {
                // 精髓所在 (关注:鲁班曰)想想next是干什么用的
                int nextIdx = patternIdx - 1;
                patternIdx = next[nextIdx];

            } else {
                mainIdx++;
            }


            if (patternIdx == pattern.length()) {
                // 这里比对完成 直接打印查找的位置
                System.out.println(mainIdx - patternIdx);
                break;
            }
        }
    }
}

总结

KMP理解后感觉还是很简单的。你不理解,会有人理解,这就是差距。建议多找几篇别人的文章和视频,有的人说的不理解,说不定有哪位大神讲的,你就恍然大悟。反复体会,然后自己实现。相信你很快也能掌握。

参考文献

最浅显易懂的 KMP 算法讲解

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值