一、KMP算法简介
KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法,效率很高,由Knuth,Morris和Pratt这三位学者发明。KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。 KMP 算法的时间复杂度为 O(m+n)。
二、算法详解
1.前缀与后缀
- 前缀:前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
- 后缀:后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
2.匹配过程
我们先假设原串为aaabaaabaaabaaaab,匹配串为aaaab。
我们在匹配的过程中各有一个指针指向原串和匹配串,这两个指针依次比对原串和匹配串中的字符是否相等,如果相等则继续匹配。
如果出现不匹配的字符,匹配串会检查之前已经匹配成功的部分中里是否存在相同的「前缀」和「后缀」。如果存在,则跳转到「前缀」的下一个位置继续往下匹配:
跳转到下一匹配位置后,尝试匹配,发现两个指针的字符无法匹配,并且此时匹配串指针前面不存在相同的「前缀」和「后缀」,这时候只能回到匹配串的起始位置重新开始:
以上就是kmp匹配算法的大致流程,这个算法的核心思想就是利用相同的前缀与后缀来省略匹配的步骤,对于相同的部分省略了重复匹配的过程来节省了匹配时间,在匹配过程中,指向原串的指针不会进行回溯。
3.next数组
在以上分析中我们发现,在遇到不匹配的字符时我们需要通过检查匹配串中的已匹配部分的相同前后缀来选择指针的跳转位置,之后再重复进行此过程,那么我们如何得知不匹配位置之前的子串中的相同前后缀的长短呢,这就需要引入next数组了。next数组的任务是找到匹配失败之后指针的跳转位置,所以next数组中每个元素的作用是记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。当我们有了这样一个next数组之后,在遇到不匹配的字符时,我们就可以通过对应的next数组元素来找到指针的跳跃点。
4.创建next数组
如果要创建一个next数组,我们首先肯定会想到通过遍历匹配串,一个一个字符进行比对,然后给每个元素赋上值,但是这样做的时间复杂度会是O(n^2),那我们如何在O(n)的时间复杂度内创建next数组呢。
我们先创建一个长度和匹配串长度相等的int数组,准备两个指针,j从0开始,i从1开始,因为单个字符是没有前后缀的,所以next[0]赋值0,然后开始遍历匹配串p,如果p[j]==p[i],next[i]=j+i,然后i和j同时++,直到p[j]!=p[i]。
如果p[j]!=p[i],把j指针指向前一位置next数组对应的值,即j=next[j-1],直到p[j] == p[i]或j == 0。如果j == 0,p[i] != p[j],next[i] = 0,i++,j不变。
重复执行上述过程,直到完成next数组的创建,时间复杂度为O(n)。
三、具体代码
class Solution {
public int[] Next(String s) {
int[] next = new int[s.length()];
int j = 0;
next[0] = 0;
for (int i = 1; i < s.length(); i++) {
while (j > 0 && s.charAt(j) != s.charAt(i)) {
j = next[j - 1];
}
if (s.charAt(j) == s.charAt(i))
j++;
next[i] = j;
}
return next;
}
public int Kmp(String haystack, String needle) {
if (needle.length() == 0) {
return 0;
}
int[] next = Next(needle);
int j = 0;
for (int i = 0; i < haystack.length(); i++) {
while (j > 0 && needle.charAt(j) != haystack.charAt(i)) {
j = next[j - 1];
}
if (needle.charAt(j) == haystack.charAt(i)) {
j++;
}
if (j == needle.length()) {
return 1;//匹配成功 返回1
}
}
return -1;//匹配失败 返回-1
}
}
- 时间复杂度:O(m + n)。