KMP算法(一种字符串匹配算法)
文章目录
kmp算法比暴力搜索快在哪里?
假设当前有目标串abcabcabda,模式串abcabd,现在要在目标串中找到模式串。
1. 最容易想到的暴力算法是怎么做的?
直接上图:
其实很简单,两层for循环解决问题,时间复杂度O(m * n)
伪代码:
for(int i = 0; i <= (m - n); i ++) {
for(int j = 0; j < n;) {
if (target[i + j] == parttern[j]) {
j++;
} else {
//失配了,提前结束
break;
}
if (j == n) {
//找到了
}
}
}
2. 那kmp算法是怎么做的?
还是先上图
看起来像是从暴力搜索算法的第一步直接跳到了第四步,但是,注意箭头的位置,是从目标串下标为5,模式串下标为2的位置开始继续比较的。
首先,先讨论为什么可以从这个位置继续比较?
可以很直观的发现一个问题,那就是在模式串里ab在下标为0
、1
和下标为3
、4
两组位置中重复出现了;而失配的位置是下标为5
的位置,也就是说明前边已经完全匹配了。那么是不是就可以把0
、1
滑到3
、4
的位置。
其次,这么做快,快在了哪里?
观察暴力搜索算法可以发现,不管是在什么位置失配,我们都要回到i+1
的位置继续搜索,而kmp算法并不需要,主串中指针不需要回溯,只要不断的后移就好了。
3. next数组求法
不难发现,目标串的下标只需要不断的后移就好了。
问题在于如何确定失配情况下模式串下一次用哪个下标与目标串当前下标比较?即接下来要说的next数组求解方法。
那么下面基于abcabd
这个模式串分析如何求解next数组:
-
首先在下标为0处失配了,那处理方法只有一种,后移目标串指针;
-
其次在下标为1处失配了,处理方法也只有一种,从模式串下标为0处开始比较;
-
下标为2处失配了,也可以观察出应该从下标为0处开始比较;
-
在下标为3处失配了,也可以观察出应该从下标为0处开始比较;
接下来就有所不同了:
-
在下标为4处失配了,可以从下标为1处比较,因为在字符串
abca
中存在最长的相同前后缀字符串a
-
而在下标5处失配了,可以从下标为2处比较,因为在字符串
abcab
中存在最长的相同前后缀字符串ab
也就是说abcabd
的next数组如下:
a | b | c | a | b | d |
---|---|---|---|---|---|
-1 | 0 | 0 | 0 | 1 | 2 |
经过分析可以发现,求next数组的问题就简化成了求最长相同前后缀字符串的问题:
那么再来求解比较复杂的模式串abacabab
的next数组
a | b | a | c | a | b | a | b |
---|---|---|---|---|---|---|---|
-1 | 0 | 0 | 1 | 0 | 1 | 2 | 3 |
基于这个数组如何求解?
for循环暴力枚举当然可以,但是还可以采用递推的方式快速求解:
-
前缀指针 j = -1;
-
后缀指针 i = 0;
-
首先,可以已知next[0] = -1,因为i = 0时不存在前缀字符串;
-
j表示的是前缀数组的指针,代表着最长前后缀字符串长度;
-
采用递推法,假设parttern[i] == parttern[j], 那么next[i + 1] = j + 1;
-
假设parttern[i] != parttern[j], 直接回到j == -1开始比较吗?
答案是否定的,可以从已经得到的next[0], …, next[j]数组入手,
即用 j = next[j]继续比较,其实就是找当前前缀数组的前缀数组继续比较。
上代码:
int[] getNext(String parttern) {
//前缀指针
int j = -1;
//后缀指针
int i = 0;
//计算字符串长度
int len = parttern.length();
//初始化数组
int[] next = new int[len];
//next[0]赋值-1
next[0] = -1;
while(i < len - 1) {
if(-1 == j || parttern.charAt(i) == parttern.charAt(j)) {
//首先当j==-1时,这是初始条件,那么为了循环能继续下去,那么i,j两个指针需要同时后移
//当parttern.charAt(i) == parttern.charAt(j)相等时i,j两个指针也需要同时后移
i++;
j++;
//思考j的含义是什么?后缀字符串的指针,是前后缀字符串最大的匹配长度
next[i] = j;
} else {
j = next[j];
}
}
return next;
}
用力扣28. 找出字符串中第一个匹配项的下标做个收尾:
class Solution {
public int strStr(String haystack, String needle) {
if(StringUtils.isEmpty(haystack) || StringUtils.isEmpty(needle)) {
return -1;
}
int[] next = getNext(needle);
int m = haystack.length();
int n = needle.length();
char[] haystackChar = haystack.toCharArray();
char[] needleChar = needle.toCharArray();
int j = 0;
for(int i = 0; i < m; i++) {
//不匹配的情况下如何回溯? 参考递推法
while(j > 0 && haystackChar[i] != needleChar[j]) {
j = next[j];
}
//那么匹配的情况下如何
if(haystackChar[i] == needleChar[j]) {
j++;
}
//结束条件
if(j + 1 == n) {
return i - n;
}
}
}
}