KMP算法
1.为什么叫KMP
因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP。
2.KMP有什么用
主要应用在字符串匹配上。
KMP的主要思想是当字符串出现不匹配的时候,可以知道一部分之前已经匹配的文本内容,从而利用这些信息避免从头匹配,减少时间复杂度。
我们使用next数组,肩负起重任。
3.什么是前缀表
next数组,其实是一个前缀表。
举一个例子,在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,在重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
那么什么是前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
4.最长相等前后缀
字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
所以字符串a的最长相等前后缀为0。字符串aa的最长相等前后缀为1。字符串aaa的最长相等前后缀为2。等等…。
5.为什么要用前缀表
下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。
6.如何计算前缀表
长度为前1个字符的子串a
,最长相同前后缀的长度为0。长度为前2个字符的子串aa
,最长相同前后缀的长度为1。长度为前3个字符的子串aab
,最长相同前后缀的长度为0。长度为前4个字符的子串aaba
,最长相同前后缀的长度为1。长度为前5个字符的子串aabaa
,最长相同前后缀的长度为2。长度为前6个字符的子串aabaaf
,最长相同前后缀的长度为0。
对于模式串为 aabaaf
, 前缀表中就存入010120
.
- 那么该如何利用前缀表,找到当字符不匹配的时候,指针应该移动的位置?
当遇到不匹配的位置的时候,查看前一位字符的前缀表的数值是多少。是多少,就将下标移动到下标为该数值的地方继续匹配。
7.前缀表与next数组
next数组可以就是前缀表,也可以是统一减一(初始位置为-1)作为next数组。
有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。
9.时间复杂度
n为文本串的长度,m是模式串的长度,匹配的过程中,根据前缀表不断的调整匹配的位置,该过程的时间复杂度是O(n),生成next数组的时间复杂度为O(m)。所以总体的时间复杂度为O(n+m)。
10.构造next数组
定义一个函数来构造next数组,函数参数为一个数组next,和一个字符串s。
public void getNext(int[] next, String s) {}
构造next数组其实就是计算模式串s,前缀表的过程。
1.初始化
定义两个指针i和j,j指向前缀起始位置,i指向后缀起始位置。
初始化j为-1
next[0] = j
2.处理前后前缀不相同的情况
因为j初始化为-1,那么就从1开始,进行s[i]与s[j + 1]的比较。
遍历模式串s的循环下标i从1开始,如果s[i]与s[j + 1]不相同,也就是遇到了前后缀末尾不相等的情况。就要向前回退。
next[j]中记录着j之前的子串的相同前后缀的长度。当遇到s[i]与s[j + 1]不同的时候,就找next[j]中的值。
while(j>=0 && s.charAt(i) != s.charAt(j+1)){
j=next[j];
}
3.处理前后缀不同情况
如果相同,那么就同时向后移动,说明找到了相同的前后缀,同时将j赋给next[i],因为要记录前后缀的长度。
这一部分的代码总体为:
public void getNext(int[] next, String s){
int j = -1;
next[0] = j;
for (int i = 1; i<s.length(); i++){
while(j>=0 && s.charAt(i) != s.charAt(j+1)){
j=next[j];
}
if(s.charAt(i)==s.charAt(j+1)){
j++;
}
next[i] = j;
}
}
使用next数组来做匹配
在文本串s中找是否出现过模式串t。定义j指向模式串的起始位置,i指向文本串的起始位置。
i从0开始遍历文本串,j从-1开始,因为next数组中记录的起始位置是-1.
比较s[i]和t[j + 1],如果不相同,j从next中寻找下一个匹配的位置。如果相同则i和j一起向后移动。
当j指向了模式串的末尾,说明在文本串s中找到了匹配的模式串。
找到模式串出现的第一个位置,所以返回当前文本串匹配模式串的位置减去模式串的长度。
完整代码
// 方法一:前缀表使用减1实现
class Solution {
public void getNext(int[] next, String s){
int j = -1;
next[0] = j;
for (int i = 1; i<s.length(); i++){
while(j>=0 && s.charAt(i) != s.charAt(j+1)){
j=next[j];
}
if(s.charAt(i)==s.charAt(j+1)){
j++;
}
next[i] = j;
}
}
public int strStr(String haystack, String needle) {
if(needle.length()==0){
return 0;
}
int[] next = new int[needle.length()];
getNext(next, needle);
int j = -1;
for(int i = 0; i<haystack.length();i++){
while(j>=0 && haystack.charAt(i) != needle.charAt(j+1)){
j = next[j];
}
if(haystack.charAt(i)==needle.charAt(j+1)){
j++;
}
if(j==needle.length()-1){
return (i-needle.length()+1);
}
}
return -1;
}
}
总结
介绍了什么是KMP,可以解决什么问题。分析了KMP算法里的next数组,知道了next数组就是前缀表。
接着一步一步推导出了前缀表,分析了KMP算法的时间复杂度。然后前缀表统一减一得到next数组,求得文本串s里是否出现过模式串t。