leetcode/notes/src/kmp.md

2.3 KiB
Raw Blame History

KMP

Leetcode 28. Find the Index of the First Occurrence in a String

KMP 主要用在 pattern 匹配上。

比如给出一个字符串 s 和一个字符串 pattern ,请找出 pattern 第一次在 s 中出现的下标。

最长公共前后缀

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

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

比如字符串 aaabcaaa 的最长公共前后缀是 aaa ,最长公共前后缀长度就是 3 。

前缀表

next[i] 表示 s[0...i] 这个子字符串的最长公共前后缀长度。

E.g.

s:      a a b a a f
next:   0 1 0 1 2 0

基本思路

j 代表 pattern 的前缀结尾,i 代表 s 的后缀结尾。

我们假设 pattern[0...j]s[i-j-1...i] 是相等的,而 pattern[0...j+1]s[i-j-1...i+1] 不想等,即末尾的 pattern[j+1]s[i+1] 不想等。

为了方便起见,我们作如下命名:

  • pattern[0...j] 为 p1
  • pattern[0...j+1] 为 p2
  • s[i-j-1...i] 为 s1
  • s[i-j-1...i+1] 为 s2
  • pattern[j+1] 为char_p
  • s[i+1] 为 char_s

那么显然有以下结论:

  • length(p1) == length(s1)
  • length(p2) == length(s2)
  • p1 == s1
  • p2 != s2
  • char_p != char_s

由于 char_p 和 char_s 不想等,因此我们需要将 j 回退到之前的某个位置重新开始匹配。

回退到哪里呢?暴力匹配算法是直接将 j 回退到 0 ,然而当 p1 的某个前缀 p1_prefix 和 s1 的某个后缀 s1_postfix 相同时,即 p1_prefix == s1_postfix 时,我们其实就可以跳过这段字符串,直接将 j 放到 p1_prefix 的末尾,然后继续尝试匹配。

那么现在的问题就是怎么找这个 p1_prefix

由于 p1 == s1 ,因此 s1_postfix 其实就是相同长度的 p1 的后缀 p1_postfix ,也就是说我们之前的假设就可以重新写为 p1_prefix == p1_postfix 。

看到了吗,其实这就是在找 p1 的最长公共前后缀。

我们找到最长公共前后缀之后,把 j 挪到 p1_prefix 的末尾就行了。

如果我们构造出了一个 pattern 的前缀表 next ,那么假设现在 j 指向的是 char_p ,把 j 往前挪的代码就可以写为

j = next[j - 1];