Table of Contents

一、朴素匹配方法

参考宫水三页题解

1
2
3
约定

本文用 pat 表示模式串,长度为 M,txt 表示文本串,长度为 N。KMP 算法是在 txt 中查找子串 pat,如果存在,返回这个子串的起始索引,否则返回 -1

直观的解法的是:枚举文本串 txt 中的每个字符作为「发起点」,每次从文本串的「发起点」和匹配串的「首位」开始尝试匹配:

匹配成功:返回本次匹配的文本串「发起点」。 匹配失败:枚举文本串的下一个「发起点」,重新尝试匹配。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution {
    public int strStr(String ss, String pp) {
        int n = ss.length(), m = pp.length();
        char[] s = ss.toCharArray(), p = pp.toCharArray();
        // 枚举原串的「发起点」
        for (int i = 0; i <= n - m; i++) {
            // 从原串的「发起点」和匹配串的「首位」开始,尝试匹配
            int a = i, b = 0;
            while (b < m && s[a] == p[b]) {
                a++;
                b++;
            }
            // 如果能够完全匹配,返回原串的「发起点」下标
            if (b == m) return i;
        }
        return -1;
    }
}

时间复杂度 O((M - N) * N)

二、KMP 算法

KMP 算法是一个快速查找匹配串的算法,它的作用其实就是本题问题:如何快速在「原字符串」中找到「匹配字符串」。

上述的朴素解法,不考虑剪枝的话复杂度是 O(m * n) 的,而 KMP 算法的复杂度为 O(m + n)。

KMP 之所以能够在 O(m + n)O(m+n) 复杂度内完成查找,是因为其能在「非完全匹配」的过程中提取到有效信息进行复用,以减少「重复匹配」的消耗。

1. 最长前缀、后缀匹配长度

在模拟 KMP 匹配过程之前,我们先建立两个概念:

前缀:对于字符串 abcxxxxefg,我们称 abc 属于 abcxxxxefg 的某个前缀。 后缀:对于字符串 abcxxxxefg,我们称 efg 属于 abcxxxxefg 的某个后缀。

前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串

后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串

image.png

最长匹配长度例子:

匹配串:aaaaak,对于k这个字符来讲最长匹配长度就是4

image-20211230174954281

2. 前缀表与 next 数组

举个例子

1
2
模式串:[aabaabsaabaabst]
next:  [-1 0 1 0 1 2 3 ...]

对于这个模式串

i = 0 时:前面没有串规定为-1

i = 1 时:前面的串为“a", 最长匹配长度为0

i = 2 时:前面的串为”aa", 最长匹配长度为1

image-20211230175748359

3. 理解 KMP 如何利用next数组加速

先来看看朴素匹配过程

  1. 将原串的指针移动至本次「发起点」的下一个位置(b 字符处);匹配串的指针移动至起始位置。

  2. 尝试匹配,发现对不上,原串的指针会一直往后移动,直到能够与匹配串对上位置。

image.png

再红标处不匹配,这时朴素做法是认为i = 0 的发起点无法匹配模式串,则i++,跳转到下一个发起点进行匹配。

再来看看KMP 匹配的做法

首先匹配串会检查之前已经匹配成功的部分中里是否存在相同的「前缀」和「后缀」。如果存在,则跳转到「前缀」的下一个位置继续往下匹配:

原因是匹配串发起点不会出现在后缀的起点之前。(反证法)

以下面的例子说明:

开始知道了i = 0 为发起点,无法匹配。f 点最长前缀为2

i = 1,必然配不出来

i = 2, 必然配不出来。

i = 3, 才有可能。

9364346F937803F03CD1A0AE645EA0F1.jpg

4. 代码实现(不包括next 数组的求法)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public static int getIndexOf(String s, String m) {
    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;
    int i2 = 0;
    int[] next = getNextArray(str2);
    while (i1 < str1.length && i2 < str2.length) {
        if (str1[i1] == str2[i2]) {
            i1++;
            i2++;
        } else if (next[i2] == -1) { // i2 == 0
            i1++;
        } else {
            i2 = next[i2];
        }
    }
    return i2 == str2.length ? i1 - i2 : -1;
}

5. next 数组求解

动态规划

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public static int[] getNextArray(char[] ms) {
    if (ms.length == 1) {
        return new int[] {-1};
    }
    int[] next = new int[ms.length];
    next[0] = -1;
    next[1] = 0;
    int i = 2;
    int cn = 0;		//  ms的第cn个字符
    while (i < next.length) {
        if (ms[i-1] == ms[cn]) {
            next[i++] = ++cn;
        } else if (cn > 0) {
            cn = next[cn];
        } else {
            next[i++] = 0;
        }
    }
    return next;
}