Skip to content

🍕 KMP 算法

KMP 主过程

【题意】 现在有两个字符串, 文本字符串 txt 和模版字符串 pat , 要求在 txt 是找出 pat 出现的下标。 没有则返回 -1

暴力搜索: 遍历 txt, 对每一个字符都判断, 以这个字符开头的字符串, 能否匹配出 pat。 暴力搜索的时间复杂度是 O(N*M), N 是 txt 长度, M 是 pat 长度。

KMP 需要借助一个 next 数组, 我们先不讲 next 数组的具体实现, 先明白 next 数组的内容就行。

了解 next 数组的构成

【字符串的最大相同真前后缀长度】: 比如 (真前后缀概念类比真子集)

  • 字符串 aaaaa 的最大相同真前后缀长度
    • 长度 4 时, 前缀 aaaa, 后缀 aaaa, 故最大相同真前后缀长度 4。 (不能是 5, 因为字符串的最大相同真前后缀长度 不可能等于字符串本身长度)
  • 字符串 aabaab 的最大相等前后缀长度
    • 长度 1 时, 真前缀 a, 真后缀 b, 不相同
    • 长度 2 时, 真前缀 aa, 真后缀 ab, 不相同
    • 长度 3 时, 真前缀 aab, 真后缀 aab, 相同
    • 长度 4 时, 真前缀 aaba, 真后缀 baab, 不相同
    • 长度 5 时, 真前缀 aabaa, 真后缀 abaab, 不相同
    • 故最大相等前后缀长度为 3

next 数组就是用来存储 pat 字符串的每个字符左侧子串(不包含该字符本身)的 最大相同真前后缀长度, 比如:

  • pat 为 aabaab
  • 下标 0 的字符 a, 左侧是空字符串, 没有 最大相同真前后缀长度, 故人为给定一个值, 比如 -1
  • 下标 1 的字符 a, 左侧只有长度为 1 的子串 a, 最大相同真前后缀长度 始终是 0
  • 下标 2 的字符 b, 左侧子串为 aa, 最大相同真前后缀长度 为 1
  • 下标 3 的字符 a, 左侧子串为 aab, 最大相同真前后缀长度 为 0
  • 下标 4 的字符 a, 左侧子串为 aaba, 最大相同真前后缀长度 为 1
  • 下标 5 的字符 b, 左侧子串为 aabaa, 最大相同真前后缀长度 为 2

KMP 主过程讲解

txt 中搜索 pat 字符串的过程中, 用到两个指针:

  • t 指针指向 txt
  • p 指针指向 pat

首先, 比对 t 指针指向的值是否等于 p 指针指向的值。 如果不相等, 则 t 指针往前走

txt:        a b c e o e S F c e o e S ..........

pat:        c e o e S F c e o e F ......



txt:      a b c e o e S F c e o e S ..........

pat:        c e o e S F c e o e F ......
  • 如果相等, 则 tp' 指针一起往前走, 继续比对
txt:    ....c e o e S F c e o e S ..........

pat:        c e o e S F c e o e F ......



txt:    ....c e o e S F c e o e S ..........

pat:        c e o e S F c e o e F ......



txt:    ....c e o e S F c e o e S ..........

pat:        c e o e S F c e o e F ......


...
...
...

txt:    ....c e o e S F c e o e S ..........

pat:        c e o e S F c e o e F ......

如果比对成功, 则直接找到, 但如果比对失败, t 指针不要动!

这就是和暴力搜索不一样的地方, 同时也是 KMP 速度快的地方。 暴力搜索慢就慢在:

  • 如果此时比对不成功, t 指针会直接回到 第一次匹配成功的下一个位置, 继续匹配。
  • 同时 p 指针也会回到开头重新匹配。

但实际上, t 指针在匹配失败的过程中, 肯定是有得到一些 "信息" 的, 暴力搜索完全忽视了这些 "信息"。 但是 KMP 利用了这些信息, 这些信息就存储在 next 数组中

首先看, 当前的 p 指针停留在 F 处, 根据 next 数组, 我们可以获取 F 左侧子串的 最大相同真前后缀长度为 4。 此时 next[p] = 4, 相同的前缀和后缀如下所示

txt:    ....c e o e S F c e o e S ..........

pat:        c e o e S F c e o e F ......
            └─────┘     └─────┘ ↑

同时, 因为 tp 是通过比对成功才走到现在这个位置的, 所以我们可以保证, 在 txt 字符串中, 同样也有这个子串 ceoe

txt:    ....c e o e S F c e o e S ..........
            └─────┘     └─────┘ ↑
pat:        c e o e S F c e o e F ......
            └─────┘     └─────┘ ↑

虽然我们这里举的是具体的例子, 但其实无论具体的值是什么, 只要 p 指针当前位置的 next[p] 值不为 0, 就一定说明存在四段相同的子串。

txt:    ....x.....x ... x ... x ? ...... 问号 ? 表示未知符号
            └─────┘     └─────┘ ↑
pat:        x.....x ... x ... x ? ...... 问号 ? 表示未知符号, 不等于上面的问号
            └─────┘     └─────┘ ↑
                \         /
                 任意长度子串, 并且一定相等

现在请你以 "懒人" 的角度思考问题, 你已经在 txt 上从位置 a 比对到位置 b 了, 见下图

            a a+1--------------->  b
txt:    ....x.....x ... x ... x ? ......
            └─────┘     └─────┘ ↑
                                t
pat:        x.....x ... x ... x ? ......
            └─────┘     └─────┘ ↑
            c                   p

然后你突然发现 tp (理解为你的两个手指) 指向的值不同了(上图中的两个问号是不相同的)。 同时你知道位置 a 到位置 b 有两段内容是一样的, 并且 txt 的 a 到 b 范围和 pat 的 a 到 b 范围是一样的, 作为一个 "懒人" , 你难道还想要把 p 回退到 c 位置(见上图), 把 t 回退到 a 的下一个位置 a+1 继续重新比对吗?

肯定不会的! 也许你此时还不知道怎么做, 但如果你是自己尝试一个一个数比对的话, 你肯定会觉得此时应该有办法 "偷懒"。 你的 t 已经走到 b 位置了, 那就说明 b 位置肯定不是 pat 的尾巴。 但是你又无法保证 a 到 b 中没有 pat 的头, 所以你陷入就纠结。

这个时候, 你想到了 txtpat 中相同的部分, 即图中的 4 块内容 , , , 是相同的

txt:    ....x.....x ... x ... x ? ......
            └─────┘     └─────┘ ↑
               甲          乙
pat:        x.....x ... x ... x ? ......
            └─────┘     └─────┘ ↑
               丙          丁

现在的情况是, , 肯定是行不通的。 但你可以再看看 行不行得通呀! 如下图:

            a --------------> b
txt:    ....x.....x ... x ... x ? ......
            └─────┘     └─────┘ ↑
               甲          乙
pat:                    x.....x ... x ... x ? ......
                        └─────┘ ↑   └─────┘
                           丙          丁

这个时候, 你就会发现:

  • 我们的 t 指针不需要动了!
  • 我们的 p 指针不用完全回到开头了!

因为我们能保证 的内容是一样的, 这个信息来自我们上一次匹配失败的过程: 虽然匹配失败了, 但是我们能保证 a 到 b 范围内是和 pat 的前缀吻合的。 (其实到这里 KMP 算法就已经讲完了。)

现在让我们从新的位置继续比对, 把图中的无用信息擦掉, 会发现图变得跟前面的很相似

txt:    ....x ... x ? ......
            └─────┘ ↑

pat:        x.....x ?.........................
            └─────┘ ↑

这不就是前面的, 两个指针指向的内容一样然后一起加加的情况嘛! 那后续的过程就简单了, 相同就一起走, 如果不同了, 就再看看 next 数组。 总而言之:

  • t 指针肯定不会往回走的
  • p 指针不好说, 如果在 next 数组中查到的值不是 0 的话, 那就可以回退一点点。

当再一次匹配不成功, 并且 p 指针对应的 next[p] 等于 0时, 就说明 当前的 ts 前面肯定没有可以匹配成功的了。 于是 p 指针只能回到开头, t 指针则向前一步继续匹配。

如此反复,

  • 如果匹配成功, 最后的下标 p 大小就会等于 pat 的数组长度
  • 如果匹配失败, 那就是 p 大小不等于 pat 的数组长度, 同时 t 指针走到头了。

KMP 的过程就是这么简单! 不懂的话, 再看看下面的代码, 就懂了。

KMP 主过程代码

【老师版本】

java
// 返回 txt_str 中 pat_str 第一次出现的下标
int getIndexOf(String txt_str, String pat_str) {
    // 边界条件不要忘记: 字符串不能空, 不能查找空串, 查找的串不能比搜索区域还大。
    if (txt_str == null || pat_str == null || pat_str.length() < 1 || txt_str.length() < pat_str.length()) {
        return -1;
    }
    char[] txt = txt_str.toCharArray();
    char[] pat = pat_str.toCharArray();
    int t = 0;
    int p = 0;
    int[] next = getNextArray(pat); // O(M) 先不用管它怎么实现的, 总之就是拿到了 pat 的 next 数组。
    // KMP 主过程
    while (t < txt.length && p < pat.length) { // O(N)
        if (txt[t] == pat[p]) { // p t 指针相同, 就一起加加
            t++;
            p++;
        } else if (p == 0) { // 等效于 next[p] == -1, 这就是为什么前面说 第一个元素的 next 值无所谓的原因
            t++; // 确保 t 前不可能匹配到 pat 的头了。
        } else {
            p = next[p]; // p 不需要直接回退到 0, 同时 t 也不需要动! KMP 的精髓就是这行代码。
        }
    }
    return p == pat.length ? t - p : -1;
}

【py版本, 已验证】:

py
def strStr(txt: str, pat: str) -> int:
    if len(txt) < len(pat):
        return -1
    t, p = 0, 0
    _next = self.getNextArr(pat)
    while t < len(txt) and p < len(pat):
        if txt[t] == pat[p]:
            t += 1
            p += 1
        elif p > 0:
            p = _next[p]
        else :
            t += 1
    return t-p if p == len(pat) else -1

证明时间复杂度 O(N)

证明 KMP 代码中的 while 循环是 O(N) 级别的:

  • 首先, 证明复杂度的难点在于, 虽然 t 是不会变小的, 但是 p 会变小, 所以一时难以知道循环会跑多少轮次。
  • 利用两个变量 tt-p,
    • t 的最大值是 N, N 表示 txt 的长度
    • t-p 的最大值也是 N, 因为 p 虽然会减小, 但最小值是 0
  • 现在分析循环中的三个分支
    • 分支一, tp 都增加, 故 t 增加, t-p 不变
    • 分支二, t 增加, p 不变, 故 t 增加, t-p 增加
    • 分支三, p 减小, t 不变, 故 t 不变, t-p 增加
  • 每次循环都会执行其中一个分支, 而不管是哪个分支, tt-p 肯定有一个值在变大
  • 现在就简单了, 之前是因为 一个变大一个变小, 我们难以知道循环会跑多要轮次。
  • 但现在两个变量 tt-p 都不可能减小, 所以就容易分析了。
  • 每次循环, tt-p 一定有一个值在增加。
  • t 最大是 N, 所以一旦 t 增加到 N 后, t 就不可能变了。 此时循环跑了 N 轮次
  • tt-p 有一定要有一个值在增加, 那么就只可能是 t-p 在增加, 也就是只可能是 p 在减小。
  • p 最多又能减小到多少呢? p 最大值是 M (M 表示 pat 长度), 最小值是 0。
  • 也就是说, p 最多能够从 M 一直减小到 0, 此时循环跑了 M 轮次
  • 所以, 循环最多跑 N+M 轮次, 考虑极端情况, M 等于 N, 那么此时也才 2N 级别
  • 综上, 时间复杂度是 O(N)

求取 next 数组 (附证明)

求取某一字符串 strnext 数组过程如下:

情况1: 长度变长

next[x], 表示的是从位置 0 到位置 x (不包含位置 x)这一个子串, 它的最大相同真前后缀的长度。

求这段子串的最大相同真前后缀长度
     ┌────────────────────┐
     0...................x-1  x

说明: 所谓真前缀, 真后缀, 类似于子集和真子集。 真前缀不能等于字符串本身, 比如字符串 abc, 前缀可能是 a,ab,abc, 但是真前缀只可能是 a,ab, 不可能是 abc 同理, 真后缀也是一样, 字符串 abc, 它的后缀可能是 c,bc,abc, 但真后缀只可能是 c,bc, 不可能是 abc

因为 "最大相同真前后缀的长度" 写起来太长了, 所以后面直接使用 next[x] 代表位置 x (不包含位置 x) 左侧字符串的 "最大相同真前后缀的长度"。

我们想要求取 next[j] 的值, 我们需要先查看 next[j-1] 的值。 假设 next[j-1] = y, 则说明 j-1 位置的左侧字符串的真前缀是 0..... y-1, 真后缀是 .....j-2。 如下:

        0............. y-1   y   .... 后面子串写到下面了, 这样方便分析, 同时也表示了 y 位置不可能是 j-1 位置, 即都是真前缀和真后缀。
        ↕               ↕    ?
        .............. j-2  j-1  j

说明: next[j-1] = y, y 是长度, 而我们的位置是从 0 开始的, 所以表示长度为 y 的字符串会写成 0......y-1。 如果字符串 0..... y-10 开头, 说明该字符串是真前缀, 其的下标范围是 [0, y-1]。 如果字符串前面没有 0, 比如 ..... j-1, 则该字符串表示的是真后缀, 具体下标范围不考虑, 因为它不影响我们理解, 只需要知道真后缀开头位置的值肯定和位置 0 的值相同。

我们记字符串 str 上某位置的字符为 str[某位置], 比如 y 位置上的字符记为 str[y], j-1 位置上的字符记为 str[j-1]str[y] 等于 str[j-1] 时, 会出现 真前缀 0.....y 等于 真后缀 .....j-1, 如下:

        0............. y-1   y   ....
        ↕               ↕    ↕
        .............. j-2  j-1  j

所以, 此时的 next[j] 就等于子串 0.....y 的长度, 即 next[j] = y+1

问题来了, next[j] 的值还可能更大吗? 答案是不可能!, 下面给出证明

证明: next[j] - next[j-1] 不可能大于 1

要证明 next[j] 不可能有更大的值, 相当于证明: next[j] - next[j-1] 不可能大于等于 2。

我们使用反证法:

  • 首先要给出前提: next[j-1] = y
  • 然后要给出假设: next[j] = e, 其中 e >= y+2
  • 最后, 以这两个作为条件, 进行推导, 看看是否有会冲突, 如果出现冲突, 则说明假设不成立, 反之假设成立。

根据条件 next[j] = e, 得到真前缀 0.....e-1 和 真后缀 .....j-1 相等 。 又根据 e >= y+2, 我们可以画出字符串如下:

        0............. y-1 ... e-1 ....      y-1 和 e-1 差值大于 1, 即中间存在新的位置, 故在中间添加 ... 作为标识
        ↕                       ↕
        ...................... j-1  j

既然真前缀 0.....e-1 和真后缀 .....j-1 相等, 那说明真前缀 0.....e-2 和真后缀 .....j-2 肯定也是相等的:

        0................ e-2   e-1 ....
        ↕                  ↕     ↕
        ................. j-2   j-1   j

也就是说, 对于位置 j-1 来说, 它的 next[j-1] 可以等于真前缀 0.....e-2 的长度, 即 next[j-1] = e-1 。 关键来了, 我们的前提是 next[j-1] = y, 而你的假设求出的 next[j-1] = e-1, 并且假设中又说了 e >= y+2。 也就是说, 根据假设, next[j-1] 存在更长的相同真前后缀, 这和我们的前提冲突, 故假设不成立, 从而证明 next[j] - next[j-1] 不可能大于 1。

情况2: 长度变短

前面讨论的情况 1 , 是 str[y] 等于 str[j-1] 的情况, 此时 next[j] 会等于 next[j-1] + 1, 如下所示:

        0............. y-1   y   ....
        ↕               ↕    ↕
        .............. j-2  j-1  j

并且我们证明了 next[j-1] + 1 就是 next[j] 的最大值。 所以, 当 str[y] 不等于 str[j-1] 时, next[j] 只会变短。 这个时候, 我们又该怎么做呢?

通过前面情况 1 的讲解, 我们应该发现了一种很方便的做法: 在前面已经求出的 next[?] 的基础上, 只需再比较 str[?] 是否等于 str[j-1] 就可以了。

注意: 此时不要去想太多前面的 next 是怎么求的, 记住他们已经求出来了, 并且求对了就行。 如果现在就开始纠结前面的 next 怎么求的, 那么你的学习效率会很低。 相反, 等到后面讲解完了, 你再思考, 那么你就会恍然大悟 (虽然正常来说, 学算法都学到 KMP 了, 应该是有这种思维的。)

这种做法很方便, 但问题在于, 我们如何确定下一个 "? 位置" 呢?

        0............. ?-1   ? ....... y-1    y ....
        ↕               ↕
        .............. j-2  j-1  j

答案就是: 看 next[ next[j-1] ] 的值。 回顾一下过程:

  • 求取 next[j] 的值
  • 先拿到 next[j-1] = y
  • 然后比较发现 str[y] 不等于 str[j-1]
  • 于是不看 y 位置了, 而是看 next[y] 位置, 即比较 str[ next[y] ]str[j-1]

为什么可以这样做呢? 不要急, 一步一步来。

先看看 next[y] 的含义, 他表示字符串 str 的位置y 左侧的子串 0......y-1 上的 "最大相同真前后缀的长度"。 若 next[y] = k 则说明真前缀 0.....k-1 和真后缀 ......y-1 相同。 如下:

字符串可以画成这样:
        0............. k-1
        ↕               ↕
        .............. y-1  y .................j-2   j-1   j
                        └───────────────────────┘
也可以画成这样:
                        ┌──────────────────┐
        0............. k-1 .............. y-1   y
        ↕                                  ↕    ≠   此时已经知道 str[y] 不等于 str[j-1], 但 str[y-1] 还是等于 str[j-2] 的
        ..................................j-2  j-1   j

因为我们是抽象讲解的, 所以可能不容易看明白, 下面我在用其他图案画一下, 目的是为了让大家发现这个时候字符串 str 的特殊子串。 此时的字符串 str, 可以确定有两部分相同真前后缀,

  • 一个是 0.....y-1.....j-2 是相同的, 这是因为 next[j-1] = y
  • 一个是 0.....k-1.....y-1 是相同的, 这是因为 next[y] = k

现在我们用特殊形状表示四段特别的区域

        0 🔺🔺🔺🔺🔺🔺 🔶🔶🔶🔶🔶🔶 y-1   y 🍕🍕🍕🍕🍕🍕 ✨✨✨✨✨✨ j-2  j-1   j

               这里面有位置 k-1, k

首先, 由于 0.....y-1.....j-2 是相同的, 所以 🔺 区域和 🍕 区域是相同的, 🔶 区域和 ✨ 区域实现相同的, 至于单个区域内部是什么, 我们不在乎, 因为他们不影响结果。 此时, 字符串将变成下面这样

        0 🔺🔺🔺🔺🔺🔺 🔶🔶🔶🔶🔶🔶 y-1   y 🔺🔺🔺🔺🔺🔺 🔶🔶🔶🔶🔶🔶 j-2  j-1   j

               这里面有位置 k-1, k

折叠一下, 比较容易看观察:

        0 🔺🔺🔺🔺🔺🔺 k-1 ... 🔶🔶🔶🔶🔶🔶 y-1   y
        ↕       ↕          ↕            ↕       ↕    ≠
        🔺🔺🔺🔺🔺🔺🔺 ....... 🔶🔶🔶🔶🔶🔶 j-2  j-1   j       中间的 ... 表示他们只是和 k-1 ... 这段的字符相同, 然后就没有什么特殊的了。 画出来只是为了严谨

记住: 0.....y-1.....j-1 是相同的。

现在再来一个条件: 0.....k-1.....y-1 相同, 所以 🔺区域 和 🔶区域也是相同的:

        0 🔺🔺🔺🔺🔺🔺 k-1 ... 🔺🔺🔺🔺🔺🔺 y-1   y
        ↕       ↕          ↕            ↕       ↕    ≠
        🔺🔺🔺🔺🔺🔺🔺 ....... 🔺🔺🔺🔺🔺🔺 j-2  j-1   j

也就是说, 现在有四段被分开的 🔺🔺🔺🔺🔺🔺 都是相同的, 我们分别标记为 1️⃣ 2️⃣ 3️⃣ 4️⃣:

                          ┌────────────────────────┐
        0 🔺🔺🔺1️⃣🔺🔺 k-1 ... 🔺🔺🔺2️⃣🔺🔺🔺 y-1    y
        ↕  ↕  ↕  ↕  ↕ ↕          ↕ ↕ ↕ ↕  ↕  ↕ ↕   ↕     ≠
        🔺🔺🔺3️⃣🔺🔺🔺 ....... 🔺🔺🔺4️⃣🔺🔺🔺 j-2   j-1   j

再重新说一遍: 由于 str[y-1]str[j-2] 是相同的, 并且 str[k-1]str[y-1] 也是相同的, 所以 str[k-1]str[j-2] 是相同的, 故我们得出, 🔺1️⃣🔺 和 🔺4️⃣🔺 是相同的:

错位一下更容易看出玄机
                                                  ┌───────────────────────────────┐
                                 0 🔺🔺1️⃣🔺🔺🔺 k-1    k  ... 🔺🔺🔺2️⃣🔺🔺🔺 y-1    y
                                 ↕                 ↕     ?
        🔺🔺🔺3️⃣🔺🔺🔺 ....... 🔺🔺🔺4️⃣🔺🔺🔺 j-2   j-1   j

现在, 明白了为什么可以直接跳到 k 位置比较 str[k]str[j-1] 了吧。 只要 str[k]str[j-1] 相同, 那么就可以得到 next[j] = k。 什么? 你问我如果又不相同要怎么办? 那就继续跳, 下一次比较的就是 str[ next[k] ]str[j-1]。 原理是一样的。

好了, 现在明白为什么可以这样后, 还需要证明它。

证明: 为什么可以跳过某些位置

先回顾一下过程:

  • next[j]
  • 先看 next[j-1] = y, 但 str[y] 不等于 str[j-1]
                  ┌─────────────────┐
       0 ....... k-1   k ......... y-1   y .......
                                    ↕    ≠
                    .............. j-2  j-1   j
  • 再看 next[ next[y] ] = k, 判断 str[k] 是否等于 str[j-1]
                                    ┌─────────────────┐
                         0 ....... k-1   k ......... y-1   y .......
                                    ↕    ?
                      .............j-2  j-1   j

为什么可以直接从 y 位置跳到了 k 位置? 换句话说, 你要如何保证 ky 之间, 不存在某个位置 a, 能够使得 真前缀 0.....a 和 真后缀 .....j-1 相同呢?

首先, 你已经理解了为什么只需比较 str[k]str[j-1], 就能够得出 0.....k.....j-1 相等的结论:

                                                ┌─────────────────────────────┐
                                 0 🔺🔺🔺🔺🔺 k-1   k ...... 🔺🔺🔺🔺🔺🔺 y-1    y
                                 ↕               ↕   ?
          🔺🔺🔺🔺🔺🔺 ....... 🔺🔺🔺🔺🔺🔺 j-2  j-1   j

但是, 这只能说明它是其中一条相同的真前后缀。 我们如何保证没有比它更长的相同真前后缀呢? 换个说明, 我们要如何证明 ky 之间, 没有一个位置 a, 能够满足 0.....a 等于 .....j-1 呢?

好, 那我们就假设存在这么一个位置 a, 并且 a 不等于 k 也不等于 y

                                 0 ......k......  a  ......  y ...
                                 ↕       ↕        ↕
                          .....  ............... j-1

同样的, 先看看前提和假设:

  • 前提
    • next[y] = k
    • 0.....y-1 等于 .....j-1 (这个大家记得这个吧, 由 next[j-1] = y 推导出来的)
  • 假设
    • 存在 a 满足 0.....a 等于 .....j-1
    • 并且 a 不是 k

既然假设 0.....a.....j-1 相等。 好, 那就说明 a 前面的字符, 和 j-1 的前面字符都是相同的:

                                 0 ......k...... a-3   a-2   a-1    a  ...... y ...
                                 ↕       ↕        ↕     ↕     ↕     ↕
                          .....  ............... j-4   j-3   j-2   j-1

同时, 因为 0.....y-1 是等于 .....j-1, 所以 str[a] 等于 str[j-1] 等于 str[y-1]。 故 a 前面的字符, 和 y-1 前面的字符, 也是相同的

            ┌────────────────────────────────────────────┐
            │                ┌───────────────────────────┼─────────────────┐
            │                │     ┌─────────────────────┼─────────────────┼─────┐
            │                │     │     ┌───────────────┼─────────────────┼─────┼─────┐
            │                │     │     │     ┌─────────┼─────────────────┼─────┼─────┼────┐
            0 ......k...... a-3   a-2   a-1    a ....    ................ y-3   y-2   y-1   y ...
            ↕       ↕        ↕     ↕     ↕     ↕
     .....  ............... j-4   j-3   j-2   j-1

也就是说, 存在 0.....a-1 等于 .....y-1。 这意味着什么? 这意味着 next[y] 的真前缀是 0.....a-1, 真后缀是 .....y-1。 也就是说, next[y] 的值可以是 a。 但我们的前提是什么? 是 next[y] = k。 我们的假设是什么? 是 a 不等于 k。 所以出现矛盾了, 假设不成立。 故不存在位置 a, 并且 next[y] != a, 还要满足 0.....a.....j-1

总结求取 next 数组过程

  • 求取 next[j]
  • 先看 next[j-1] 是多少, 假如 next[j-1] = y
  • 比较 str[y] 是否等于 str[j-1] 。 (因为我们求的是 j 位置的 next, 所以不管怎么变, 都是要和 str[j-1] 相比较的)
    • str[y] 等于 str[j-1], 则可以直接求出 next[j] = y+1。 (为什么不可能大于 y+1 前面已经给出证明)
    • str[y] 不等于 str[j-1], 则继续:
  • 查看 next[y] 是多少, 假如 next[y] = k
  • 比较 str[k] 是否等于 str[j-1]。 (为什么不需要看 k-y 之间的值前面也给出了证明)
  • 重复上面过程, 每次都会往回跳, 直到
  • 查看 next[?] 是多少, 发现 next[?] = 0
  • 比较 str[0] 是否等于 str[j-1]
    • 如果相等, 说明 next[j-1] = 0+1
    • 如果相等, 就直接赋值 next[j-1] = 0
    • 结束求取 next[j] 的过程。

代码

【老师版本, 带注释】:

java
int[] getNextArray(char[] str) {
    // KMP 主流程中算法控制了传入的 str 不可能是 空
    if (str.length == 1) {
        return new int[] {-1};  // 首元素位置的值是什么其实无所谓
    }
    int[] next = new int[str.length];
    next[0] = -1;
    next[1] = 0;
    int j = 2; // 要求的 next[j]
    int yk = next[j-1]; // 或者直接写成 yk = 0。 yk 就是等待比较的那个字符的位置下标, 即前面讲解中的 y, 然后是 k, ....
    while (j < next.length) {

        if (str[j-1] == str[yk]) { // 求取 next[j] 时, 始终都是和 str[j-1] 比较, 变化的只会是前面的跳板
            // next[j] = yk+1;  // 匹配到 0......yk-1 等于 ......j-1, 故 next[j] = yk, 因为长度 = yk-1+1
            // j += 1; // 计算出 next[j] 后, 接着计算下一个
            // yk = next[j-1]; // 每次计算 next[j] 时, 都会先获取 next[j-1] 的值。
            // 上面三行代码, 可以直接简写成下面一行代码
            next[j++] = ++yk; // 涉及到证明1
        } else if (yk > 0) {
            // 直接往前跳, next[yk] 到 yk 部分不用看, 肯定找不到满足的。 涉及到证明 2
            yk = next[yk];
        } else {
            next[j] = 0; // 跳到 0 位置了还不相等, 说明没有相同真前后缀
            j += 1; // 继续求下一个元素的 next
            // yk 不需要变, 因为 yk 等于 0了, 下一个值的 next 最多也就是 1
        }

    }
    return next;
}

【py版本, 已验证】:

py
def getNextArr(pat):
    le = len(pat)
    if le == 1:
        return [-1]
    _next = [0] * le
    j = 2
    yk = _next[j-1]
    while j < le:
        if pat[j-1] == pat[yk]:
            _next[j] = yk + 1
            j += 1
            yk = _next[j-1]
        elif yk > 0:
            yk = _next[yk]
        else:
            _next[j] = 0
            j += 1
    return _next

证明时间复杂度 O(M)

证明方式和 KMP 的证明方式几乎一模一样, 甚至更简单:

  • 创建两个变量, jj-yk
  • j 的最大值是 M, M 表示 str 的长度, 也就是 KMP 中的 pat 长度
  • j-yk 最大值也是 M, 因为 yk 最小值是 0
  • 查看循环三个分支
    • 分支1, jyk 都加加, 故 j 变大, j-yk 不变
    • 分支2, yk 变小, 故 j 不变, j-yk 变大
    • 分支3, j 加加, 故 j 变大, j-yk 变大
  • 三个分支, 无论走哪一个, jj-yk 都会有一个值变大
  • 而循环结束条件又只需要 j 达到 M
  • 故极端情况下, j-yk 一直变大, 循环轮次 M 次
  • 然后就只能 j 变大了, 循环轮次 M 次
  • 最多循环 2M 次, 故时间复杂度是 O(M)

🍕 KMP 算法搜索所有匹配的字符串

A A A A A B A A A B A
      |
A A A A

此时匹配完毕 txt[0] 匹配完毕, 按照暴力搜索的方法, 它会直接重置 pat 指针 j 回到 0, 但实际上 pat 指针可以继续留在这个位置上。 原因是前面的内容我们已经比较过了, 是相等的, 没必要再比较。

这个时候 lps 数组出来了, 如果此时的 j 是 4, lps[4] = 3, 那么利用 j = lps[4] 就可以让 j 下一次继续停留在下标 3 位置上。

lps[i] 等于 子串 pat[0...i] 的最长前缀, 但要求这个前缀不能等于子串的长度。 比如 lps[0] 表示求取长度为 1 的字符串的 lps, 因为不能等于本身, 所以 lps[0] = 0。 而计算 lps[1] 时, 会判断长度为 2 的字符串 lps, 这个时候就需要比较了, 可能是 1,可能是0。 这就是和 next 数组不一样的地方。

  • “AAAA”, lps[] is [0, 1, 2, 3]
  • “ABCDE”, lps[] is [0, 0, 0, 0, 0]

求解 lps:

利用 len 变量求解, lenpat 的下标, 它始终指向子串的前缀最后一个字符。 所以, 过程如下:

  • 如果 pat[len] 等于 pat[i], 说明目前的前缀和后缀是匹配的
    • len += 1
    • lps[i] = len
    • i += 1
  • 如 pat[len] 不等于 pat[i], 说明目前的前缀和后缀不匹配, len 需要退一个再比较看看
    • len -= 1
    • 如果 len 退到 0 后, 都还不匹配, 则说明 没有前缀和当前子串的后缀匹配, 故 lps[i] = 0
  • 注意, lps[i] 只可能是一个一个递增的。 如果 len 回退到 0 后, 后面的 lps[i] 是不可能突然变大的, 只能是继续慢慢+1+1

KMP 怎么使用 lps

  • 首先, pat[j] 和 txt[i] 字符比较
  • 如果 ✔️, 则 i 和 j 一起加加
    • 当 j 匹配完了, 由于 j++, 所以 j 会等于 pat 的长度。 此时需要更新 j = lps[j-1] (这一点是 next 数组没有的)
  • 如果 ❌
    • 此时我们知道前面有一些字符是匹配的, 即 pat[...j-1] 和 txt[...i-1] 是匹配的。 因为 j 只会在匹配时才会增加
    • 同时我们也知道 lps[j-1] 的值, 表示的是 pat[...j-1] 的最大真前后缀长度
    • 通过这两点, 我们可以得出, 我们不需要匹配 txt[i-j, i-1] 这部分内容, 因为这部分内容肯定是匹配的。 (这一点和 next 数组一样)
    • 也就是说, 此时可以通过 lps[j-1] 获得下一次匹配的 pat 下标, 而 i 是不需要移动的。 (这一点和 next 数组不太一样)

lps 数组的优势在于查找字符串出现次数时, 可以有效的减少匹配次数, 比如匹配 AAAAA 和 AAA 时。 next 数组, 当找到一个后就结束了。

最开始, i 和 j 都从 0 开始。

A A A A A B A A A B A
0
A A A A
0

✔️, i++, j++


A A A A A B A A A B A
  1
A A A A
  1
✔️, i++, j++


A A A A A B A A A B A
    2
A A A A
    2
✔️, i++, j++


A A A A A B A A A B A
      3
A A A A
      3
✔️, i++, j++
j++ 后发现 j==4 匹配完毕, 不像暴力搜索那样, 它会直接将 j 重置回 0。
通过 lps[j-1], 我们可以得到 j = lps[4-1] = 3, 即 j 还会继续指向下标 3


A A A A A B A A A B A
        4
  A A A A
        3
✔️, i++, j++
j==4, j = lps[4-1] = 3


A A A A A B A A A B A
          5
    A A A A
          3
❌, j = lps[j-1]
此时发现不匹配, 于是 j 根据 lsp[j-1] 重置回 2


A A A A A B A A A B A
          5
      A A A A
          2
❌, j = lps[j-1] = 1


A A A A A B A A A B A
          5
        A A A A
          1
❌, j = lps[j-1] = 0


A A A A A B A A A B A
          5
          A A A A
          0
❌, 因为 j==0, 所以 让 i++


A A A A A B A A A B A
            6
            A A A A
            0
✔️, j++, i++


A A A A A B A A A B A
              7
            A A A A
              1
✔️, j++, i++


A A A A A B A A A B A
                8
            A A A A
                2
✔️, j++, i++


A A A A A B A A A B A
                  9
            A A A A
                  3
❌, j = lps[j-1]


A A A A A B A A A B A
                  9
              A A A A
                  2
❌, j = lps[j-1]


A A A A A B A A A B A
                  9
                A A A A
                  1
❌, j = lps[j-1]


A A A A A B A A A B A
                  9
                  A A A A
                  0
❌, j == 0, 故 i++


A A A A A B A A A B A
                    10
                    A A A A
                    0
END

🍕 KMP 算法总结

实际上, next 数组和 lps 数组是一模一样的。 前面 next 数组中的 KMP 算法, 只不过是在找到后就退出了, 想要修改的话, 只需要将循环结束条件中的 M < len(pat) 去掉就可以了。 然后添加一个判断, 如果 M 等于 pat 的长度时, 下一个匹配的元素应该继续维持在 pat 的最后一个元素, 这样就和 lps 数组的实现一样了。

然后就是 next 数组和 lps 数组的求法, 也都是一样的, 变量都是用来保存当前位置的最长前后缀。 至于数组中的值, lps 数组只是比 next 快一格罢了, lps 数组的第一个元素值固定是 0, 而 next 数组的前两个元素值固定是 -1, 0。

  • next[j] 求取的子串是 pat[...j-1]
  • lps[j] 求取的子串是 pat[...j]

所以本质上 next 和 lps 数组是一样的, 只不过一个包含自己, 一个不包含自己而已。 他们求取的都是最长前后缀, 并且长度不等于字符串本身。

然后就是求取的方法, 两者本质上是一样的。

  • next 每次跳的元素数量是不一定相同的, 但它确保每次跳的位置, 都是前缀的最后一个元素。 因为每次都是根据 next[?] 的值进行跳的
  • lps 每次跳的元素数量也是根据前面的 lps 求取出来的。 并且也能保证每次跳的位置都是前缀的最后一个元素。 方法也是产品主前面的 lps[?] 的值。

lps 的求取, 相当于 next 求取过程中, 比较的是 pat[j] 而不是 pat[j-1]。 即包不包含元素本身这一个区别罢了。