题目
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 ****。
示例 1:
输入: haystack = "sadbutsad", needle = "sad"
输出: 0
解释: "sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入: haystack = "leetcode", needle = "leeto"
输出: -1
解释: "leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
提示:
1 <= haystack.length, needle.length <= 104haystack和needle仅由小写英文字符组成
作用
KMP 算法的作用是当出现字符串不匹配时,可以记录一部分之前已经匹配的内容,避免从头再匹配。
前缀表
前缀表的作用是回退,它记录了模式串和文本串不匹配的时候,模式串应该从哪里重新开始匹配。
例子:
要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
前缀表要求的其实是相同前后缀的长度。
这里面有两个概念,前缀和后缀。
前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
如何求前缀表
根据上图,可以口算一下每一个子串的相同前后缀的长度。
前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
- 子串为 a 时,相同前后缀长度为 0 。
- 子串为 aa 时,相同前后缀长度为 1 。
- 子串为 aab 时,相同前后缀长度为 0 。
- 子串为 aaba 时,相同前后缀长度为 1 。
- 子串为 aabaa 时,相同前后缀长度为 2 。
- 子串为 aabaaf 时,相同前后缀长度为 0 。
如下图所示:
实现
前缀表实现时可以统一减一。如下图所示:
先贴代码,再分析。
const getNext = (needle) => {
let next = [];
let j = -1;
next.push(j);
for (let i = 1; i < needle.length; ++i) {
while (j >= 0 && needle[i] !== needle[j + 1])
j = next[j];
if (needle[i] === needle[j + 1])
j++;
next.push(j);
}
return next;
}
首先,需要明确概念。
-
next 数组每个元素是模式串即
aabaaf每个下标对应的相同前后缀的长度。 next[0] 对应的是aabaaf中 字符a的相同前后缀长度。同时也是相同前后缀中前缀尾元素下标(统一减一后)。 -
代码中的 j 对应的就是模式串下标为 i 时,最长相等前后缀的长度。
-
因为 next 数组统一减一,所以将 j 初始化为 -1.
在明确了这几个概念之后,代码就很容易理解了。就是比较相同前后缀中的前缀的后一个元素即needle[j + 1]和相同前后缀中后后缀的后一个元素即needle[i],并进行操作。
题目解法
这道题目,逻辑和求 next 数组很相似。
var strStr = function (haystack, needle) { if (needle.length === 0) return 0;
const getNext = (needle) => {
let next = [];
let j = -1;
next.push(j);
for (let i = 1; i < needle.length; ++i) {
while (j >= 0 && needle[i] !== needle[j + 1])
j = next[j];
if (needle[i] === needle[j + 1])
j++;
next.push(j);
}
return next;
}
let next = getNext(needle);
let j = -1;
for (let i = 0; i < haystack.length; ++i) {
while (j >= 0 && haystack[i] !== needle[j + 1])
j = next[j];
if (haystack[i] === needle[j + 1])
j++;
if (j === needle.length - 1)
return (i - needle.length + 1);
}
return -1;
};