From 414e4719a35b83ca091bf8515eebb62226489bfe Mon Sep 17 00:00:00 2001 From: Sainnhe Park Date: Thu, 1 Dec 2022 09:55:22 +0800 Subject: [PATCH] KMP Update --- notes/src/kmp.md | 54 +++++++++++++++---- ...ex_of_the_first_occurrence_in_a_string.cpp | 53 ++++++++---------- 2 files changed, 66 insertions(+), 41 deletions(-) diff --git a/notes/src/kmp.md b/notes/src/kmp.md index 108e98b..9d8fbfd 100644 --- a/notes/src/kmp.md +++ b/notes/src/kmp.md @@ -1,16 +1,16 @@ # KMP -[代码随想录](https://programmercarl.com/0028.%E5%AE%9E%E7%8E%B0strStr.html) +[Leetcode 28. Find the Index of the First Occurrence in a String](https://leetcode.com/problems/find-the-index-of-the-first-occurrence-in-a-string/) KMP 主要用在 pattern 匹配上。 -比如给出一个字符串 s 和一个 pattern ,请找出 pattern 第一次在 s 中出现的下标。 +比如给出一个字符串 s 和一个字符串 pattern ,请找出 pattern 第一次在 s 中出现的下标。 ## 最长公共前后缀 -前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串; +前缀是指**不包含最后一个字符**的所有以第一个字符开头的连续子串; -后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。 +后缀是指**不包含第一个字符**的所有以最后一个字符结尾的连续子串。 比如字符串 aaabcaaa 的最长公共前后缀是 aaa ,最长公共前后缀长度就是 3 。 @@ -18,18 +18,50 @@ KMP 主要用在 pattern 匹配上。 `next[i]` 表示 `s[0...i]` 这个子字符串的最长公共前后缀长度。 +E.g. + +```text +s: a a b a a f +next: 0 1 0 1 2 0 +``` + ## 基本思路 -`i` 代表 pattern 的前缀结尾,`j` 代表 s 的后缀结尾。 +`j` 代表 pattern 的前缀结尾,`i` 代表 s 的后缀结尾。 -我们假设 `pattern[0...j]` 和 `s[i-j...i]` 一开始是相等的,但是当 `j` 和 `i` 都自增了 1 之后就不完全相等了,即末尾的 `pattern[j]` 和 `s[i]` 不相同。 +我们假设 `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]` 有一段前缀 `pattern[0...k]` 和 `s` 的一段后缀 `s[i-k...i]` 相同。 +为了方便起见,我们作如下命名: -那么我们可以从 `pattern[k+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 -这个时候我们需要让 `j` 回退到 `k+1` 。那么怎么做呢? +那么显然有以下结论: -实际上 `k+1 == next[j-1]` 。 +- length(p1) == length(s1) +- length(p2) == length(s2) +- p1 == s1 +- p2 != s2 +- char_p != char_s -参考 s0028 详细代码实现与注释 +由于 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 往前挪的代码就可以写为 + +```cpp +j = next[j - 1]; +``` diff --git a/src/s0028_find_the_index_of_the_first_occurrence_in_a_string.cpp b/src/s0028_find_the_index_of_the_first_occurrence_in_a_string.cpp index f801884..80a4ce4 100644 --- a/src/s0028_find_the_index_of_the_first_occurrence_in_a_string.cpp +++ b/src/s0028_find_the_index_of_the_first_occurrence_in_a_string.cpp @@ -1,37 +1,35 @@ #include "s0028_find_the_index_of_the_first_occurrence_in_a_string.hpp" // 构造字符串 s 的前缀表 -// next[i] 表示 s[0]~s[i] 这个子字符串的最长相同前后缀的长度 -// 一个字符串的前缀是指从 0 开始往后到不包含末尾字符的子字符串 -// 一个字符串的后缀是指从末尾开始往前到不包含第一个字符的子字符串 -// 比如 aaba 拥有相同的最长前后缀 a 和 a -// 前缀表的一个例子: -// s: a a b a a f -// next: 0 1 0 1 2 0 // 在 getNext() 函数中,我们假设 s.length() >= 2 void S0028::getNext(int* next, const string& s) { // j 有两重含义: // 1. 前缀的末尾下标 // 2. 最长相同前后缀的长度 + // i 的含义是后缀的末尾下标 + // 初始化 j 为 0 int j{0}; // next[0] 很显然一定是为 0 的 next[0] = 0; - // 开始迭代,每次迭代我们都会填充 next[i] - // i 的含义是后缀的末尾下标 - // i 从 1 开始迭代,因为 length >= 2 - // 我们没必要考虑 i == 0 的情况 + // 开始迭代 + // 我们的目标是在每次迭代的过程中,找到最长相同前后缀 + // s[0...j] == s[?...i] + // i 从 1 开始迭代,因为 length >= 2 ,我们没必要讨论 + // i == 0 的情况 int len = s.size(); for (int i{1}; i < len; ++i) { // 当 s[j] 和 s[i] 不想等时,即前后缀不匹配的时候 // 前缀末尾的下标 j 需要进行回退 - // 回退到什么位置呢? // a a a f a a a f // j i - // j i + // 回退到什么位置呢? // 注意观察,s[j] 和 s[i] 虽然不想等,但是前面这一段 // aaafaaa 有着公共前后缀 aaa ,所以我们可以试着跳到 // 前缀 aaa 的后面那个元素的位置 f,然后比较前缀 aaaf // 和后缀 aaaf 是否相同。 + // a a a f a a a f + // j i + // j i // 由于前缀和后缀都有着公共的 aaa ,所以我们只需要比较 // s[j] 和 s[i] 是否相同就行了。 // 如果不相同,继续回退,直到 j 回退到起始位置 0。 @@ -57,39 +55,34 @@ void S0028::getNext(int* next, const string& s) { } int S0028::strStr(string haystack, string needle) { - int stringLen = haystack.size(); - int patternLen = needle.size(); - if (patternLen == 0) { + int haystackLen = haystack.size(); + int needleLen = needle.size(); + if (needleLen == 0) { return 0; } - // 开始创建 pattern 的前缀表 - int next[patternLen]; + // 开始创建 needle 的前缀表 + int next[needleLen]; getNext(next, needle); - // j 用来索引 pattern ,它有两层含义 + // j 用来索引 needle ,它有两层含义 // 1. 前缀的末尾下标 // 2. 最长相同前后缀的长度 int j = 0; // 开始迭代 // 接下来的操作和 getNext() 中的迭代非常相似 - // i 用来索引 string ,它的含义是后缀末尾下标 - // 不过我们这里有个假设,那就是每次迭代开始的时候 - // string[i-j...i] 和 pattern[0...j] 相同 - // 注意,现在 i 从 0 开始迭代,之所以不像 getNext() + // 不过注意,现在 i 从 0 开始迭代,之所以不像 getNext() // 中那样从 1 开始迭代是因为 getNext() 不需要考虑 i == 0 - for (int i{0}; i < stringLen; ++i) { - // 不想等,回退 j ,思路和 getNext 一样 - // 回退之后要么 j == 0 ,要么 string[i-j...i] - // 和 pattern[0...j] 相同 + for (int i{0}; i < haystackLen; ++i) { + // 首先讨论末尾不匹配的情况,我们需要回退 j while (j > 0 && needle[j] != haystack[i]) { j = next[j - 1]; } - // 相等,那好说,直接往前推,和 getNext() 一样 + // 接下来处理末尾相同的情况,那好说,直接往前推 if (needle[j] == haystack[i]) { ++j; } // 成功找到匹配字符串,返回 - if (j == patternLen) { - return i - patternLen + 1; + if (j == needleLen) { + return i - needleLen + 1; } } return -1;