Orion's Studio.

算法(29)-strStr 和 KMP

2024/03/14

题目(easy)

找出字符串中第一个匹配项的下标

给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

思路

KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。

KMP

什么是 KMP

由这三位学者发明的:Knuth,Morris和Pratt。

为什么要用 KMP

当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。

核心问题搞清楚:next数组里的数字表示的是什么,为什么这么表示?

什么是前缀表

前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。

如要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。

前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

最长公共前后缀

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

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

前缀表要求的就是相同前后缀的长度。

为什么要用前缀表

下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。

如何计算前缀表

下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

前缀表与next数组

这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)

使用next数组来匹配

以下我们以前缀表统一减一之后的next数组来做演示。

时间复杂度分析

其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。

暴力的解法显而易见是O(n × m),所以KMP在字符串匹配中极大地提高了搜索的效率。

构造next数组

构造next数组其实就是计算模式串s,前缀表的过程。要有如下三步:

  1. 初始化
  2. 处理前后缀不相同的情况
  3. 处理前后缀相同的情况

使用next数组来做匹配

next数组里记录的起始位置为-1。

1
2
3
4
5
6
7
8
9
10
11
12
int j = -1; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.size(); i++) { // 注意i就从0开始
while(j >= 0 && s[i] != t[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t
return (i - t.size() + 1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* @param {string} haystack
* @param {string} needle
* @return {number}
*/
var strStr = function (haystack, needle) {
if (needle.length === 0)
return 0;

const getNext = (needle) => {
let next = [];
let j = 0;
next.push(j);

for (let i = 1; i < needle.length; ++i) {
while (j > 0 && needle[i] !== needle[j])
j = next[j - 1];
if (needle[i] === needle[j])
j++;
next.push(j);
}

return next;
}

let next = getNext(needle);
let j = 0;
for (let i = 0; i < haystack.length; ++i) {
while (j > 0 && haystack[i] !== needle[j])
j = next[j - 1];
if (haystack[i] === needle[j])
j++;
if (j === needle.length)
return (i - needle.length + 1);
}

return -1;
};

时间复杂度: O(n + m)。
空间复杂度: O(m), 只需要保存字符串needle的前缀表。

CATALOG
  1. 1. 题目(easy)
  2. 2. 思路
  3. 3. KMP
    1. 3.1. 什么是 KMP
    2. 3.2. 为什么要用 KMP
    3. 3.3. 什么是前缀表
    4. 3.4. 最长公共前后缀
    5. 3.5. 为什么要用前缀表
    6. 3.6. 如何计算前缀表
    7. 3.7. 前缀表与next数组
    8. 3.8. 使用next数组来匹配
    9. 3.9. 时间复杂度分析
    10. 3.10. 构造next数组
    11. 3.11. 使用next数组来做匹配