目录
KMP算法是一种字符串匹配算法,大大提高了字符串匹配的效率
例题:力扣28. 实现 strStr(),以下是根据该题的题解而写
1、什么是前缀函数
前缀函数是KMP算法的核心,记作 π(i),其定义如下:对于长度为m的字符串,其前缀函数 π(i)(0≤i<m) 表示 ss 的子串 s[0:i] 的最长的相等的真前缀与真后缀的长度(真前缀、真后缀就是不等于自身的的前缀与后缀)。特别地,如果不存在符合条件的前后缀,那么 π(i)=0。
我们举个例子说明:字符串 aabaaab 的前缀函数值依次为 0,1,0,1,2,2,3。
π(0)=0,因为 a 没有真前缀和真后缀,根据规定为 0(可以发现对于任意字符串 π(0)=0 必定成立);
π(1)=1,因为 aa 最长的一对相等的真前后缀为 a,长度为 1;
π(2)=0,因为 aab 没有对应真前缀和真后缀,根据规定为 0;
π(3)=1,因为 aaba 最长的一对相等的真前后缀为 a,长度为 1;
π(4)=2,因为 aabaa 最长的一对相等的真前后缀为 aaa,长度为 2;
π(5)=2,因为 aabaaa 最长的一对相等的真前后缀为 aa,长度为 2;
π(6)=3,因为 aabaaab 最长的一对相等的真前后缀为 aab,长度为 3。
2、如何求前缀函数
首先要先直到几个性质
① π(i) ≤ π(i - 1)+1
该性质很容易理解。π(i) 比 π(i - 1) 多了一个字符串,如果多的这个字符串与 π(i - 1) 的前缀的后一个索引的字符相等,那前缀函数就会多一
② 如果 s[i] = s[π(i−1)],那么 π(i) = π(i−1) + 1
由于 π(i−1) 是真前缀和真后缀相等的长度,那么 s[π(i−1)] 就是 π(i−1) 的真前缀的下一个字符,若该字符与 s[i] 相等,那么 π(i) 必然等于 π(i−1) + 1
至此,根据性质②,如果s[i] = s[π(i−1)],那么我们就可以确定 π(i) = π(i−1) + 1
而如果 s[i] != s[π(i−1)],易知前缀就必须要从 s[0 .. π(i−1) - 1] 中寻找。π(i) <= π(π(i−1) + 1),此时继续判断 s[i] 与 s[π(π(i−1) - 1)],若相等,根据性质②求出 π(i);如不相等,则要继续判断 s[i] 与 s[π(π(π(i−1) - 1))],不断递归。设 j = π(π(π(…)−1)−1),那么我们就要不断递归,直到 s[i] = s[j] 或者 j = 0,π(i) = π(j)
3、解题思路
不断遍历 haystack 字符串和needle 字符串,期间不断与 needle 字符串比较,若遍历完 needle 字符串,说明该字串匹配。若 haystack[i] != needle[j],根据前缀函数的定义,我们只需将 needle 的指针移到 π[j - 1] 即可,然后继续遍历。若无法遍历完 needle 字符串,则说明没有匹配的字串。
4、代码
class Solution {
public int strStr(String haystack, String needle) {
int n = haystack.length(), m = needle.length();
if (m == 0) {
return 0;
}
int[] pi = new int[m]; // 前缀函数,pi[0] = 0
for (int i = 1, j = 0; i < m; i++) {
// 不断递归,直到 j = 0 或 needle[i] == needle[j]
while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
j = pi[j - 1];
}
// 若needle[i] == needle[j],则pi[i] = pi[j] + 1
if (needle.charAt(i) == needle.charAt(j)) {
j++;
}
pi[i] = j;
}
for (int i = 0, j = 0; i < n; i++) {
// 不同,则根据前缀函数跳转needle指针到pi[j - 1]索引
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = pi[j - 1];
}
// 相同,两个指针都右移一位
if (haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if (j == m) {
return i - m + 1;
}
}
return -1;
}
}