# kmp 讲解
代码随想录 (programmercarl.com)
当我们需要在一个字符串中查找另一个字符串的出现位置时,常常会使用 KMP 算法,因为它在时间复杂度上具有优势。接下来,我将详细讲解 KMP 算法的原理和步骤。
假设我们需要在文本串 S 中查找模式串 P 的出现位置。
-
预处理模式串 P:
-
首先,我们需要构建一个部分匹配表(Partial Match Table),它记录了模式串 P 中每个位置的最长相等前缀和最长相等后缀的长度。
-
部分匹配表的构建是通过递归的方式完成的,从模式串的第二个字符开始,依次比较前缀和后缀的字符。如果相等,部分匹配值加一;如果不相等,则需要根据已经计算出的部分匹配值来决定模式串的偏移量。
-
举个例子,假设模式串 P 为 "ABCDABD",那么部分匹配表如下:
部分匹配表: [0, 0, 0, 0, 1, 2, 0]
-
-
执行匹配操作:
- 在文本串 S 中,维护两个指针 i 和 j,分别指向当前正在比较的字符位置。
- 从文本串 S 的第一个字符开始,逐个与模式串 P 的字符进行比较。如果相等,则 i 和 j 分别往后移动一位;如果不相等,则根据已经计算得到的部分匹配表,决定模式串 P 的偏移量。
- 偏移量的计算方法为:将 j 移动到部分匹配表对应位置的值,即 j = 部分匹配表 [j]。
- 如果模式串 P 的 j 指针移动到了开头(即 j=0),说明当前字符与文本串 S 的字符不匹配,此时 i 和 j 分别向后移动一位。
- 重复以上步骤,直到找到模式串 P 在文本串 S 中的出现位置,或者遍历完整个文本串 S。
- 如果匹配成功,模式串 P 在文本串 S 中的出现位置为 i-j。
KMP 算法通过利用部分匹配表,可以在比较过程中跳过一些不必要的字符比较,提高了匹配的效率。这是因为部分匹配表保存了模式串 P 的前缀和后缀的最长相等长度,使得我们可以根据已经匹配到的字符信息,移动模式串 P 的位置,避免无效的比较操作。
构建 next 数组(也称为部分匹配表)是 KMP 算法的一部分,它用于预处理模式串 P。以下是一种常见的构建 next 数组的方法:
- 初始化 next 数组:创建一个长度为模式串 P 长度的数组,初始值都为 0。
- 遍历模式串 P,从第二个字符开始(即下标为 1),依次计算每个位置的最长相等前缀后缀长度。
- 对于第 i 个位置,首先令 j 等于前一个位置的 next 值,即 j = next [i-1]。
- 在一个循环中,比较模式串 P 的第 i 个字符和第 j 个字符:
- 如果 P [i] == P [j],说明存在一个更长的相等前缀后缀,因此对应的 next [i] = j + 1,并将 i 和 j 分别向后移动一位(i++,j++)。
- 如果 P [i] != P [j],说明当前字符不匹配,需要回溯。这时,j 指针需要根据已经计算得到的 next 数组来进行偏移,即 j = next [j]。
- 如果 j 已经回溯到开头(即 j=0),说明没有找到相等前缀后缀,此时对应的 next [i] 为 0,并将 i 向后移动一位(i++)。
- 重复步骤 4,直到遍历完整个模式串 P,得到最终的 next 数组。
下面举个例子来说明,假设模式串 P 为 "ABCDABD":
- 初始化 next 数组为 [0, 0, 0, 0, 0, 0, 0]。
- 从第二个字符开始,依次计算每个位置的最长相等前缀后缀长度。
- 对于第二个位置,比较 P [1] 和 P [0],不相等,所以 next [1] = 0。
- 对于第三个位置,比较 P [2] 和 P [0],不相等,所以 next [2] = 0。
- 对于第四个位置,比较 P [3] 和 P [0],不相等,所以 next [3] = 0。
- 对于第五个位置,比较 P [4] 和 P [0],相等,所以 next [4] = 0+1 = 1,即最长相等前缀后缀长度为 1。
- 对于第六个位置,比较 P [5] 和 P [1],相等,所以 next [5] = 2。
- 对于第七个位置,比较 P [6] 和 P [2],不相等,比较 P [6] 和 P [1],不相等,比较 P [6] 和 P [0],所以 next [6] = 0。
- 遍历完模式串 P,得到最终的 next 数组为 [0, 0, 0, 0, 1, 2, 0]。这个数组保存了模式串中每个位置的最长相等前缀后缀长度。
通过以上步骤,我们成功地构建了 next 数组。在 KMP 算法的匹配过程中,利用这个数组可以根据已经匹配到的字符信息,移动模式串的位置,避免不必要的比较操作,提高匹配效率。
# 题目
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
示例 1: 输入: haystack = "hello", needle = "ll" 输出: 2
示例 2: 输入: haystack = "aaaaa", needle = "bba" 输出: -1
说明:当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr () 以及 Java 的 indexOf () 定义相符。
28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)
# 分析
// 时间复杂度: O (n + m) | |
// 空间复杂度: O (m), 只需要保存字符串 needle 的前缀表 | |
class Solution { | |
public: | |
void getNext(int* next, const string& s) { | |
int j = -1; | |
next[0] = j; | |
for(int i = 1; i < s.size(); i++) { // 注意 i 从 1 开始 | |
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 | |
j = next[j]; // 向前回退 | |
} | |
if (s[i] == s[j + 1]) { // 找到相同的前后缀 | |
j++; | |
} | |
next[i] = j; // 将 j(前缀的长度)赋给 next [i] | |
} | |
} | |
int strStr(string haystack, string needle) { | |
if (needle.size() == 0) { | |
return 0; | |
} | |
int next[needle.size()]; | |
getNext(next, needle); | |
int j = -1; //// 因为 next 数组里记录的起始位置为 - 1 | |
for (int i = 0; i < haystack.size(); i++) { // 注意 i 就从 0 开始 | |
while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配 | |
j = next[j]; //j 寻找之前匹配的位置 | |
} | |
if (haystack[i] == needle[j + 1]) { // 匹配,j 和 i 同时向后移动 | |
j++; //i 的增加在 for 循环里 | |
} | |
if (j == (needle.size() - 1) ) { // 文本串 s 里出现了模式串 t | |
return (i - needle.size() + 1); | |
} | |
} | |
return -1; | |
} | |
}; |