KMP
推荐视频
例题
28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)
算法
先假设有一个待搜索字符串t
,和一个模式串p
传统:暴力匹配
暴力匹配以i = 0
至 i = n - m
为开头依次比较p串的每一位是否相同,全相同则匹配成功
KMP
最大公共前后缀
需要注意的是,这里的前后缀都不包括字符串本身
以kelvin
这一字符串为例
前缀: {k, ke, kel, kelv, kelvi}
后缀: {n, in, vin, lvin, elvin}
我们使用int d[]
来保存最大公共前后缀,d[j]
表示p[0..j]
的最大公共前后缀
最大公共前后缀可以带来什么好处
KMP算法通过计算p串已匹配部分的==最大公共前后缀==,以减少已经符合规则的p串部分(在遇到一个与t串不匹配的字符时,不是抛弃所有已匹配的字符,而是通过D得到p串已匹配字符中可以利用的部分(最大公共前后缀))
i
↓
t : ...aaaaaab...
p : aaaab
↑
j
如上面的例子,t[i]
与p[j]
无法匹配。
这时p[0..j-1]
是匹配成功,且已匹配成功的最大公共前后缀为d[j-1]
,如何计算d我会在后文给出,这里我们先通过肉眼观察到d[j-1]
为3。
由此可知,t串不匹配字符的前3个字符是与p串的前3个字符相同的,所以我们可以直接对t串的第i个字符与p[3]进行匹配,而无需从头对p串进行匹配
如何计算最大公共前后缀
由于最大公共前后缀不包括字符串本身,所以d[0] = 0
这里其实与上小节思路相同,也是在某一字符匹配失败时,尝试使用已匹配成功的最大公共前后缀来减少比较次数。
p : aaaab
p : aaaab
代码
// LC-28
class Solution {
public int strStr(String haystack, String needle) {
int n = needle.length(), m = haystack.length();
if (n == m) {
return haystack.equals(needle) ? 0 : -1;
} else if (n > m) {
return -1;
}
// d[x]表示,needle[0] ~ needle[x] 区间的最大公共前后缀
// 特别地:前缀不包括needle[x],后缀不包括neddle[0];
int[] d = new int[n];
// 所以当只有一个字符时,最大公共前后缀为0
d[0] = 0;
for (int i = 1, j = 0; i < n; i++) { // 错开一位
while (needle.charAt(i) != needle.charAt(j) && j > 0) { // 第j位发生了不匹配
// 说明前needle[0 .. j - 1]位是匹配的
// 获取前j - 1位的最大公共前后缀
j = d[j - 1];
}
if (needle.charAt(i) == needle.charAt(j)) {
j++;
}
d[i] = j;
}
for (int i = 0, j = 0; i < m; i++) {
while (haystack.charAt(i) != needle.charAt(j) && j > 0) {
j = d[j - 1];
}
if (haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if (j == n) {
return i - n + 1;
}
}
return -1;
}
}