KMP算法 左神 最传统 最详细的思路 JAVA

Stella981
• 阅读 689

本文只是一个学习后的总结,可能会有错误,欢迎各位指出。任意转载。

题目:给定一个字符串 str1 和一个字符串 str2,在字符串 str1 中找出字符串 str2 出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

str1 = aaaaabcabc

str2 = abcabcaa

前段时间偶然接触到左神的算法讲解视频,大概三天的时间,反反复复把 KMP 算法看了三遍。终于有了一些自己的理解与体会。用传统的 KMP 算法去做字符串匹配,其实是用 next 数组对暴力算法的一个优化。另外一种理解是将 KMP 算法理解为动态规划,这里不详细叙述。

这里我分为三部分来讲。

  1. 暴力解法
  2. KMP 算法
  3. 如何求 next 数组

暴力解法

暴力算法看起来非常简单,实际编码还是需要处理一些细节的,建议写一写。这里的给 str1 一个 i 指针,给 str2 一个 j 指针。i 的第一个初始位置是 0,最后一个初始位置是 str1.length - 1。

  1. str1[ i ] 和 str2[ j ]相等: i 和 j 都往后移动一位。

  2. str1[ i ] 和 str2[ j ]不等,j 归 0, i 从下一个初始位置开始比较。

如果 j 能够到 length 这个位置上,说明从第 0 位到第 str2.length - 1 位都已经相等了,此时返回 i - j ,就是 str2 在 str1 中出现的第一个位置的 index 。

如果 i 到达了最后一个初始位置,也就是 str1.length - 1 ,此时还没有匹配成功,那么说明永远都没办法匹配到 str2 。 这个时候返回 -1 。

代码:

public int strStr(String str1, String str2) {
    int length1 = str1.length();
    int length2 = str2.length();
    if(length2 == 0) return 0;
    if(length1 < length2) return -1;
    int i = 0;
    while(i < length1){
        int j = 0;
        while(i < length1 && j < length2
        && str1.charAt(i) == str2.charAt(j)){
            i++;
            j++;
        }
        if(j == length2){
            return i-j;
        }
        i = i - j + 1;
    }
    return -1;
}

还是建议动手写一下。

KMP算法

这里暂时先不讨论 next 是如何来的。你需要知道它存放的是 str2 的一些信息。他的值等于 str2 前面的所有字符形成的字串的前缀等于后缀的最大值。这里非常绕,举个例子来说明:

index 等于 6 的时候, 字串是 a b c a b c

前后缀取 1 的时候,前缀为 a, 后缀为 c,此时不等。 next 不能取 1 。

前后缀取 2 的时候,前缀为 ab, 后缀为 bc, next 不能取 2 。

前后缀取 3 的时候,前缀为 abc, 后缀为 abc, 此时相等, next 可以取 。

前后缀取 4 的时候,前缀为 abca, 后缀为 cabc, next 不可以取 4 。

前后缀取 5 的时候,前缀为 abcab, 后缀为 bcabc, next 不可以取 5 。

前后缀不可以取 6 。因为前后缀不可以为字符串本身。

index:0 1 2 3 4 5 6 7 8 9

str1 = a a a a a b c a b c

str2 = a b c a b c a a

next:-1 0 0 0 1 2 3 1

接下来是 KMP 算法的流程。按照暴力的解法,我们还是有两根指针 i 和 j。

  1. 两个元素相等时: i 和 j 往后移动一位。
  2. 两个元素不等时: j = next [ j ],如果此时 next[ j ] 等于 -1,说明 j 指针已经移动到了最前面。

我们来仔细理解这个不相等的两种情况,这里是难点。

next[j] != -1,这种情况下,j 指针直接跳到 str2[next[j]]去。为什么这样做可以?举例子。

index 0 1 2 3 4 5 6 7

str1 = a b c f a b c x

str2 = a b c f a b c y

next=-1 0 0 0 0 1 2 3

在 index 为 6的时候,i = j = 7,这个时候两个元素不相等,我们会把 j 跳到 str2[next[j]],也就是 j = 3。这个时候 str1 前面的子串 和 str2 前面的子串是相等的,他们拥有共同的 next 数组。j 跳到 3,这个 3 代表: y/x 前面这个子串他的前三位和后三位相等。那么,我们的 y 的子串前三位 和 x 子串的后三位这个时候是不是就不需要比较了,因为这个 3 默认了他们相等。那么前三位(index为 0 1 2)就不需要比较了,直接比较第四(index 为 3 )位。这里就是 next 数组的核心。在左神的视频里面讲得更直观。

str1 = a b c f a b c x

str2 = * * * * a b c f a b c y

比较 x 与 f 是否相等。

next[j] == -1,这种情况下,j 已经来到最前面了,没办法继续前移,那么只能 i 向后移。

代码:

public static int getIndexOf(char str1[], char str2[]) {
        if(str1.length == 0 || str1.length < str2.length) {
            return -1;
        }
        if(str2.length == 0) {
            return 0;
        }
        int i = 0;
        int j = 0;
        int next[] = getNextArray(str2);
     //对应三种情况
        while( i < str1.length && j < str2.length) {
            if(str1[i] == str2[j]) {
                i++; //两个元素相等
                j++;
            }else if(next[j] == -1) {
                i++; //next[j] == -1
            }else {
                j = next[j];//next[j] != -1
            }
    } 
    return (j == str2.length) ? i-j : -1;
    }

next数组

str2 = a b c f a b c y

next=-1 0 * * * * * *

第一位默认为 -1。 因为第一位元素没有子串。

第二位默认为 0。因为第二位元素的子串只有一个元素,那他的前后缀最大相等数目只能为0。

接下来是第三位,第三位的子串是a b,这里是难点。如何求出它的 next 值。j = 3

用 j - 1 的 next 的值,cn = next[j-1]的 str2 对应的元素, 和 str2[j-1] 比较。这里的cn = 0, 那比较的就是第 0 号元素和第 1 号元素的值。比较出来一定有两种情况,相等,不相等。而在不相等的时候又要分两种情况。

index 0 1 2 3 4 5 6 7

str2 = a b c f a b c y

next=-1 0 0 0 0 1 * *

为了更直观看见,我换个例子。j = 6.

cn = next[j-1] = 1, str2[cn] = b

str2[j-1] = b

这个时候是相等的,因此 next[6] = ++cn = 2 。为什么?

这个 cn 代表的是什么?cn 代表的是 j-1 位的 next 值,这个值代表 j-1 位的前后缀最大值。这个最大值是 1,说明他第一位和最后一位相等。那么比较他的第二位(str2[cn])和最后一位的下一位(str2[j-1])是否相等。相等的话,next[6] = ++cn = 2 。不相等怎么办?分为两种情况。

  1. cn > 0,cn = next[cn]
  2. cn<= 0,next[j] = 0

这里又是为什么,就是在子串的情况下继续分,去找到和str[j-1]相等的 cn,如果一直找不到呢?怎么办,那next[j] = 0

代码:

public static int[] getNextArray(char []str) {
    if(str.length == 1) {
        return new int [] {-1};
    }
    int next[] = new int [str.length];
    next[0] = -1;
    next[1] = 0;
    int i = 2;
    int cn = 0;
    while( i < str.length) {
        if(str[i-1] == str[cn]) {
            next[i++] = ++cn;
        }else if(cn > 0) {
            cn = next[cn];
        }else {
            next[i++] = 0;
        }
    }
    return next;
}

小结一下

  1. 暴力解法,多去写,多写两遍就熟练。
  2. KMP 具体实现,有三种情况。元素相等、元素不等且 next 不等于-1、元素不等且 next 等于 -1。
  3. next 的求解方法,也是三种情况。cn 和 j-1 对应的元素相等、 对应的元素不等且cn>0、 对应的元素不等且cn<=0。

公众号 stul 同步更新算法学习过程,欢迎关注。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
2年前
strcmp函数实现及分析
最近看C,看到strcmp函数,对它的实现原型不很清楚,于是到网上搜。网上算法一大堆,看了很多代码后自己做了一下总结strcmp函数是C/C中基本的函数,它对两个字符串进行比较,然后返回比较结果,函数形式如下:intstrcmp(constchar\str1,constchar\str2);其中str1和str2可以是字
Wesley13 Wesley13
2年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
DaLongggggg DaLongggggg
3年前
python百题大冲关-确定字符串是否是另一个的排列
挑战介绍实现一个算法来识别一个字符串str2是否是另一个字符串str1的排列。排列的解释如下:如果将str1的字符拆分开,重新排列后再拼接起来,能够得到str2,那么就说字符串str2是字符串str1的排列。本次挑战中,你需要在permutation.py文件中补充函数is_permutation的空缺部分
Wesley13 Wesley13
2年前
java采坑之路
判断相等字符串判断相等        String str1  null;        String str2  "java金融";       // str1.equals(str2);  错误的写法        str2.equals(str1); // 常量写前面        Objects.equ
Wesley13 Wesley13
2年前
SHELL IF条件判断,判断条件
1字符串判断str1str2      当两个串有相同内容、长度时为真str1!str2     当串str1和str2不等时为真\nstr1       当串的长度大于0时为真(串非空)\zstr1       当串的长度为0时为真(空串)str1          当串str1为非空时为真
Easter79 Easter79
2年前
SpringBoot自定义序列化的使用方式
场景及需求:项目接入了SpringBoot开发,现在需求是服务端接口返回的字段如果为空,那么自动转为空字符串。例如:\    {        "id":1,        "name":null    },    {        "id":2,        "name":"x
Stella981 Stella981
2年前
JavaScript常用函数
1\.字符串长度截取functioncutstr(str,len){vartemp,icount0,patrn/^\x00\xff/,strre"";for(vari
Wesley13 Wesley13
2年前
VIM和sed 替换字符串方法
(1)VIM替换字符串方法1\.基本替换:s/str1/str2/替换当前行第一个str1为str2:s/str1/str2/g替换当前行所有str1为str2:n,$s/str1/str2/替换第n行开始到最后一行中每一行的第一个str1为str2:n,$s/str1/str2/g替换第n行开始到最后一行中每
Stella981 Stella981
2年前
SpringBoot自定义序列化的使用方式
场景及需求:项目接入了SpringBoot开发,现在需求是服务端接口返回的字段如果为空,那么自动转为空字符串。例如:\    {        "id":1,        "name":null    },    {        "id":2,        "name":"x