KMP
KMP算法简介
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。**KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。**具体实现就是通过一个next()函数
实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)
。
字符串的模式匹配
字符串的模式匹配是一种常用的运算。所谓模式匹配,可以简单地理解为在目标(字符串)中寻找一个给定的模式(也是字符串),返回目标和模式匹配的第一个子串的首字符位置。通常目标串比较大,而模式串则比较短小
如下图:
S1
是目标串,S2
是模式串,要在S1
中寻找S2
,最后返回S2
出现在S1
中的首字符的位置。
先思考一下朴素做法:
可以定义两个指针 i
和 j
,i
用于遍历S1
而 j
用于遍历S2
。
一开始i
,j
都是指向字符串的首位,发现不相等,S2
字符串不断向后移动,如果发现 i
和 j
所指向的字符相等,i
和 j
都向后面移动,继续判断两个指针所指向的字符是否相等。
最终,指针j
遍历完S2
后发现所有字符都匹配上了,于是就返回第一个子串的首字符位置: i - j + 1
。
以上情况是成功找到的情况,但是实际可能遍历到最后都没有匹配成功,并且浪费了大量的时间和资源。
如上图所示,如果当目标串和模式串都比较大时,时间复杂度会非常高: O(n*m)
所以需要进行改进。在遍历过程中,一定会出现一些字符的冗余比较,而kmp算法中核心的next
就是为了避免冗余的比较而设计的,next
数组中每一个数对应每一个个字符的下标,而这每一个数都代表着一个信息,就是前面的字符串中最长且相等的前缀和后缀的长度。同时要注意,next
数组是与模式串向匹配的信息。
比如上图,字符 c 之前的最长且相等的前缀串和后缀串是 abc 并且长度是3,所以next
数组在 c 位置上的信息就是 3 。
如上图,next数组
中保存的信息就会起到作用,在 i
移动到字符 t 的位置, j
移动到字符 c 位置的时候,发现不相等那么根据 c 位置对应的next数组
的信息是 3 ,那么就可以将j移动到 3 的位置再进行比较。
如果发现 i
和 j
指向的字符还是不等的话就再根据next数组
中对应的信息将j指针回退到一定的位置。这样就可以大幅度的减少 j
指针移动的次数和比较字符的次数,从而降低时间复杂度。
实现KMP算法
JAVA代码:
public class KMP {
public static int getIndexOf(String s1, String s2) {
if (s1 == null || s2 == null || s2.length() < 1 || s1.length() < s2.length()) {
return -1;
}
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
int x = 0, y = 0;
int[] next = getNextArray(str2);
while (x < str1.length && y < str2.length) {
if (str1[x] == str2[y]) {
x++;
y++;
} else if (next[y] == -1) {
x++;
} else {
y = next[y];
}
}
return y == str2.length ? x - y : -1;
}
public static int[] getNextArray(char[] match) {
if (match.length == 1) {
return new int[]{-1};
}
// 定义next数组
int[] next = new int[match.length];
next[0] = -1;// 第一个字符前没有任何字符,所以特殊处理,就规定它对应的next数组值是-1
next[1] = 0; // 第二个字符前面只有一个字符,所以最大相等的前缀和后缀长度一定是1
int i = 2; // 在那个位置上求next数组的值
int cn = 0;
while (i < next.length) {
if (match[i - 1] == match[cn]) { // 匹配成功的时候
// 这一行代码实际就是下标换算的过程,包含三个操作: (1)将cn++ (2)将cn+1后的值赋给next[i] (3) 将i向后移动也就是i++
next[i++] = ++cn;
} else if (cn > 0) {
// 上面的判断不成立, 就对cn做回退处理
cn = next[cn];
} else {
// 如果cn回退到0的位置了,那么说明没有相等的前缀和后缀,就将此处设置为0
next[i++] = 0;
}
}
return next;
}
}
简化后的模板代码:
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
while (j > 0 && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
while (j > 0 && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m)
{
j = ne[j];
// 匹配成功后的逻辑
}
}
例题:
acwing 831. KMP字符串
leetcode 28. 实现 strStr()
leetcode 214. 最短回文串