【深入浅出理解KMP算法】

「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」。

前言

大家好,我是程序猿小白 gw_Gw,很高兴能和大家一起学习进步。
复制代码

以下内容部分来自于网络,如有侵权,请联系我删除,本文仅用于学习交流,不用作任何商业用途。

摘要

本文主要介绍KMP算法以及next数组的求法。
复制代码

KMP算法

1. KMP算法的概念和应用

【概念】

KMP算法全称是Knuth-Morris-Pratt 算法,没错就是Knuth,Morris,Pratt这三个人一起研究出来的字符串匹配算法,通过已经匹配过的字符来减少回溯匹配的次数。

【应用】

给定主串和模板串,如果模板串在主串中存在,就返回对应的索引,否则返回-1。

这类题就是KMP算法的典型应用。

2. KMP算法实践

这里通过leetcode的一道算法题来介绍KMP算法的实现。

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。 链接:leetcode-cn.com/leetbook/re…

这道题就是KMP算法的经典应用。

2.1 KMP算法分析

在使用KMP算法分析之前我们需要了解字符串前缀和后缀的定义。

  • 字符串前缀:必须包含第一个字符,但不包含最后一个字符的连续子串。(aba 的字符串前缀有:a ab)
  • 字符串后缀:必须包含最后一个字符,但不包含第一个字符的连续子串。(aba 的字符串后缀有: ba a)

知道了这两个定义我们就可以来进行分析了,举个栗子:主串是abababcdsd 模式串是 abcdsd

image-20220130214727549

按照暴力匹配的算法我们先对两个字符串的首位开始比较,如果相同,那么就对下一位比较,如果不同就从第二位也就是当前主串的下一位开始比较,一直循环直到比较的主串的剩余长度小于模式串的长度。(都小于模式串长度了,肯定不会存在了。)

而KMP算法则是在暴力匹配的基础上进行了优化。

下面是比较过程:

image-20220130215850181

image-20220130215937623

image-20220130220128890

发现c前面是ab,ab的前缀是a,后缀是b,不相同,所以模式串的箭头回到a的位置

image-20220130220536696

继续进行匹配。

image-20220130220639043

image-20220130220735865

image-20220130220904053

image-20220130221003201

image-20220130221109888

可以明显看到后面都是相等的,这里就不再描述。从这里我们可以看到,实际上一直都是模式串的指针在回溯,而主串的指针式一直向前的,这大大减少了我们的回溯操作。到这里你或许还不明白这样为什么能确保我们不回溯主串能找到正确结果呢,其实最关键的点就是相同前缀和后缀。上面的栗子没有相同的前缀和后缀,我们对字符串进行小小的修改。

image-20220130222616197

在我们进行到第二位的比较时会遇到不相同的情况。所以我们进行回溯。

image-20220130222712956

仍然不相同,继续回溯,但这时我们已经是0位置了,因此我们不需要再回溯了,我们把主串的指针前进一位。

image-20220130222812451

发现相等了,同时前进一步,同样相等,继续前进一步,直至不相等。

image-20220130223007292

经过判断发现有相同的前缀和后缀a,于是c前面的a就是我们需要的,因为模式串前面有个相同的前缀,而有后缀说明在主串中也有相同的字符串,否则模式串的指针不会往后移动。即在碰到不匹配的字符串时,如果前面有相同的前缀和后缀(最长的),那么意味着,在主串中同样拥有和前缀相同的字符串,这时我们就可以将主串中不匹配的位置的字符和模式串中前缀的后面一个位置(前缀的长度)开始比较,因为前面相同的了,于是:

image-20220130224056693

相当于:

image-20220130224252701

接着继续匹配,a和a相等,a和c不相等,判断c前面有相同的前后缀,根据前面的描述移动位置:

image-20220130224615500

一直到最后匹配成功。

接下来我们总结一下主要步骤。

【小结】

挨个匹配,如果相同就同时前进,如果不同就判断模式串和主串不同的字符的位置前面的子串中有没有相同的前缀和后缀,如果有,找出来最长的,如果没有,回溯模式串的指针,回退到不相等的位置的前一个位置,继续进行匹配。

2.2 next数组

我们已经知道了KMP算法的基本思想,但是我们如何实现呢?这就是我们要介绍的next数组,next数组实际上就是存放该位置的前面的字符串的最长相同前缀和后缀的长度。可以帮助我们进行模式串的回退,也就是说当遇到冲突时,指向我们的下一步,这也是next数组的名字的由来。

那么怎么来求next数组呢?其实知道了next数组的作用,求next数组就好理解多了。

next数组的求法

  1. 设置两个指针len和pos,一个初始值为0,一个初始值为1,循环让两个指针对应的字母进行比较。

    1. 如果相同则让len的值加一,
    2. 如果不同则设置让len为pos的前一位的值,继续比较,直到len的值为0或者遇到相等的字符(这里注意是要循环进行回退)。
  2. 让next[pos] = len;

举个栗子

模式串:ababc。

image-20220206163514998

image-20220206163916269

image-20220206175042416

image-20220206170107066

image-20220206170237233

image-20220206170552111

得到了最终结果我们发现,下标0存放的的是下标1前面的字符串拥有的相同前后缀长度,后面依次都是此规律,因此我们再遇到冲突时要回退到它的前一位的数组值,即找到该字符串前面的相同的前缀和后缀长度。

来看具体代码:

public void getNext(String pattern,int[] next){
        //初始化
        int len = 0;
        for (int pos = 1; pos < pattern.length(); pos++) {
            //如果没有相同前后缀,向前回退
            while(len>0 && pattern.charAt(len)!=pattern.charAt(pos)){
                len = next[len-1];
            }
            //如果有相同前后缀,len++
            if(pattern.charAt(len)==pattern.charAt(pos)){
                len++;
            }
            next[pos] = len;
        }
}
复制代码

也有些求next数组时,是吧整个数组右移或者减一,但是原理都是一样的,只不过匹配时代码有所差异,大家可以对比理解。

2.3 KMP算法实例

理解了KMP算法的作用以及如何next数组的求法,接下来我们就利用KMP算法来解决这道题。为了方便看,我把题目再复制一遍。

【题目】

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

思路:

  1. 求得next数组,以便找到不匹配时回退的位置。

  2. 初始化两个指针,分别指向文本串和模式串的开始位置,即初始化为0.

  3. 循环对比文本串和模式串

    1. 如果相同,模式串指针++
    2. 如果不同,根据next数组找到模式串回溯的位置。(此过程为循环)
    3. 如果模式串的指针已经到了最后,说明已经匹配到了,返回文本串的指针-模式串的长度+1即可。
  4. 如果最后没有匹配到就返回-1.

【代码实现】

public int strStr(String haystack, String needle) {
        //如果是空串,返回0
        if(needle.length()==0){
            return 0;
        }
        int[] next = new int[needle.length()];
        //获取next数组
        getNext(needle,next);
        //初始化指针
        int i = 0;
        for (int j = 0; j < haystack.length(); j++) {
            //如果不相等。进行回溯
            while(i>0 && haystack.charAt(j)!=needle.charAt(i)){
                i = next[i-1];
            }
            //如果相等,i++
            if(haystack.charAt(j)==needle.charAt(i)){
                i++;
            }
            //如果i到了最后一位,说明匹配到了
            if(i==needle.length()){
                return j-needle.length()+1;
            }
        }
        return -1;
    }
复制代码

小结

以上就是关于KMP算法的介绍,以及next数组的求法,最后给出了leetcode的一道题来实践,希望能对读者有所帮助,如有不正之处,欢迎留言指正。

Guess you like

Origin juejin.im/post/7061562409202221092