每日算法总结——KMP算法详解(包含Java实现)

KMP算法解决的问题:字符串str1和str2,str1 是否包含str2,如果包含返回str2在str1中开始的位置。如何做到时间复杂度 O ( N ) O(N) O(N)完成?(经典字符串匹配问题

暴力解法:遍历str1中的每个字符,判断以该字符为首字符时,能否与str2匹配,时间复杂度为 O ( N ∗ M ) O(N*M) O(NM)

每日算法总结——KMP算法详解(包含Java实现)_第1张图片

不难发现经典解法的实质就是,如果i位置为首的字符串不匹配,就往回跳到i+1从头开始判断,仔细看整个过程可以知道,每次回跳,都会重复判断很多字符,那是否可以将这些信息收集利用起来,使得每次不用回跳这么多?KMP算法就是这样的,它甚至不用回跳。

讲解KMP之前,先来了解几个概念:

  • 最大相同前、后缀长度:对于一个字符串str的第i位置的字符str[i],其最大相同前后缀长度指的是在这个字符之前的子串(即str[0~i-1])所具有的最长相同前后缀的长度,以abbabbk为例:

    • 对于k这个字符,其对应的子串为abbabb
      1. 当取长度为1时,其前缀为a,后缀为b,不相同(不匹配❌);
      2. 当取长度为2时,其前缀为ab,后缀为bb,不相同(不匹配❌);
      3. 当取长度为3时,其前缀为abb,后缀为abb,相同(匹配成功✔);
      4. 当取长度为4时,其前缀为abba,后缀为babb,不相同(不匹配❌);
      5. 当取长度为5时,其前缀为abbab,后缀为bbabb,不相同(不匹配❌);
      6. 不能取长度为6,因为6就是这个子串的长度,一定会匹配,判断它没意义
    • 这样我们就得出最大前后缀匹配长度为3

在这里插入图片描述

  • next数组:就是由最大相同前后缀长度(还挺拗口)构成的数组,该数组长度等于字符串长度,字符串中每个字符的最大相同前后缀长度都记录在该数组中。

    • 对于0位置处的字符,由于其前面没有子串,所以人为规定其最大相同前后缀长度为-1
    • 对于1位置处的字符,其前面只有一个字符,由于其前后缀一定相同,所以我们将其设为0

    举个栗子:

    字符串:[ a a b a a b s a a b a a b s t]
    next: [-1 0 1 0 1 2 3 ...]
    

KMP算法详解:

我们有了next数组,就可以根据next数组的信息,来决定匹配开始的位置,具体过程如下:

  • 我们要在str1中查找str2,就计算出str2的next数组。

  • 假如现在从str1[i]位置开始匹配,匹配到str1[X]位置发现不相等,即str1[X]≠str2[Y],由str2的next数组我们可知Y位置之前字符串的最大前后缀匹配长度,也就是下图中的橙圈,既然前后缀相同,我们就可以直接省略str2前缀的匹配(即从位置j开始的匹配),直接从可能不相等的位置开始验证(即判断str1[X]str2[X-j]是否相同)。
    每日算法总结——KMP算法详解(包含Java实现)_第2张图片

  • 如何认定以[i, j]范围上任意一个字符为首的字符串一定无法与str2匹配呢?

    • 在从i位置往下匹配的过程中,当我们来到X,发现str1[X]≠str2[Y],这时我们才停止匹配,所以在str1[i, X-1]str2[0, Y-1]范围上的字符串一定相等。

    • 假设在str1[i, j]范围上存在一个字符str1[k],使得以该字符开头,可以得到一个与str2相匹配的子串。则易知str1[k, X-1]范围上子串一定与str2等量的部分相同(即紫色框框的部分)
      每日算法总结——KMP算法详解(包含Java实现)_第3张图片

    • 不难发现现在对于str2[Y]来说,已经出现了一个更长的最大相同前后缀,就是两个紫色的部分(因为之前我们已经推出了str1[i, X-1]=str2[0, Y-1]的结论),这是与next的结论相悖的,所以不成立。

  • 如何求next数组?

    • next[i]代表什么?

      • 0~i-1字符串的最大相同前后缀长度
      • 即下图中的下标k:
        在这里插入图片描述
    • 首先,之前已经说过:next[0] = -1, next[1] = 0

    • 对于next[i],我们可以利用next[i - 1]的信息计算,next[i - 1]是字符str2[i - 1]之前子串的最大相同前后缀长度,也就是下图中的j橙圈代表前/后缀:
      在这里插入图片描述

    • 首先我们要判定j位置的字符是否等于i-1位置的字符

      • 如果它们相等的话,显然next[i]=next[i-1] + 1 = j + 1(上图紫圈

      • 如果他们不等的话,则需要让j往前跳:j = next[j],因为两个橙圈的部分是相同的,都是str2[i-1]最大相同前后缀,但同时也是str2[j]之前的字符串,所以next[j]是什么呢?,没错,就是下图中的k绿色圈 标出了str2[j]之前的字符串的最大相同前后缀
        在这里插入图片描述

        所以有j = next[j] = k,然后判断j位置的字符(即str2[k])是否等于i-1位置的字符,重复上述过程……

      • j到达0位置时,说明并没有公共前缀,所以next[i] = 0

JavaCode:

public class Kmp {

    /**
     * @param s 在哪个字符串中匹配
     * @param m 要匹配的字符串
     * @return 首个匹配字符串的首字符下标。如果没有匹配,则返回-1
     */
    public static int getIndexOf(String s, String m) {
        // 要求 N >= M, N = s.length, M = m.length
        if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
            return -1;
        }
        char[] str1 = s.toCharArray();
        char[] str2 = m.toCharArray();
        int i1 = 0, i2 = 0;
        // O(M)
        int[] next = getNext(str2);
        // O(N)
        while (i1 < str1.length && i2 < str2.length) {
            if (str1[i1] == str2[i2]) {
                i1++;
                i2++;
            } else if (i2 > 0) {
                // 不匹配,i2往前跳
                i2 = next[i2];
            } else {
                // str2中比对的位置已经无法往前跳了
                i1++;
            }
        }
        // i2越界,说明匹配成功;i1越界,说明匹配失败;(同时越界也代表匹配成功)
        return i2 == str2.length ? i1 - i2 : -1;
    }

    /**
     * 获取ms的next数组
     */
    public static int[] getNext(char[] ms) {
        if (ms.length == 1) {
            return new int[]{-1};
        }
        int[] next = new int[ms.length];
        next[0] = -1;
        next[1] = 0;
        // i: next数组的位置
        int i = 2;
        // cn: 要和next[i-1]比较的位置
        int cn = 0;
        while (i < next.length) {
            if (ms[i - 1] == ms[cn]) {
                next[i++] = ++cn;
            } else if (cn > 0) {
                // 当前跳到cn位置的字符,和i-1位置的字符配不上,则cn继续向前
                cn = next[cn];
            } else {
                next[i++] = 0;
            }
        }
        return next;
    }

    public static void main(String[] args) {
        System.out.println(getIndexOf("abbsabb", "sabb"));	// 3
    }
}

复杂度分析:

  • 假设str1和str2的长度分别是N和M
  • getNext
    • 首先对于while循环中的内容,存在两个量ii - cn
      • 第一个条件分支中,i增加,i - cn不变
      • 第二个条件分支中,i不变,i - cn加一
      • 第三个条件分支中,ii - cn都增加
    • 这两个量在整个while循环中,要么整体增加,要么只增加一个,而两个量的范围都是[0, M],所以整体复杂度为 O ( 2 M ) = O ( M ) O(2M)=O(M) O(2M)=O(M)
  • getIndexOf中的while循环
    • 同样存在两个量i1i1 - i2
      • 第一个条件分支中,i1增加,i1 - i2不变
      • 第二个条件分支中,i1不变,i1 - i2增加
      • 第三个条件分支中,i1i1 - i2都增加
    • 可以发现,这两个量在整个while循环中,要么整体增加,要么只增加一个,而两个量的范围都是[0, N],所以整体复杂度为 O ( 2 N ) = O ( N ) O(2N)=O(N) O(2N)=O(N)
  • 所以整个算法的复杂度为 O ( M + N ) = O ( N ) O(M+N)=O(N) O(M+N)=O(N)

实战

  • LeetCode原题:28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)

  • 难度Medium

有一说一,左神讲KMP算法讲的是真的好,不过可能我明天就忘了_(:3」∠)_,还是需要不断巩固啊

你可能感兴趣的:(每日算法,算法,数据结构,java)