KMP算法详解
作者:木子六日;
日期:2021年3月8日;
介绍
问题引入
假设有一个字符串a和字符串b;
若b是a子串,返回b在a中的位置,如a="xxyyzz",b="zz"
,则返回4;
若b不是a的子串,则返回-1;
暴力解法
暴力显然是可以做的,我们判断a每一个位置作为开头能否匹配出b即可,时间复杂度就是a.length*b.length
;
KMP算法其实也是这个思路,只不过回退的策略发生改变,不再是每次都傻傻的回退到最前面;
next数组的介绍
next数组是用来帮助KMP实现最省事的回退。
他的概念是这样的(这里先说下概念,求法后面有):
next数组的长度和b相同,next[i]表示b[i]前面的所有字符的最长的相同前后缀(不包含前面的整个字符串);
举个例子,b="aacaacaade"
,那么next[8]就是字符d前面字符串"aacaacaa"
,他的相等的前后缀有"a","aa","aacaa"
,但是最长的是5位,所以next[8]=5。
我们规定next[0]=-1,next[1]=0。
KMP算法流程
过程
我们不再回退a了,只回退b,而且回退的位置由next数组决定。
我们一开始都是从字符串1和2的0位置开始匹配,发现f
和g
不相等了,按照暴力的解法,这时候字符串1就需要跳回到1位置的b
再由1位置作为起始位置和字符串2做匹配;
而KMP的做法是这样的:
- 字符串1不动;
- 将字符串2的位置跳回到next[i]的位置,i位置标示字符不匹配的位置;
- 如果还不匹配,接着跳,next[next[i]];
- 若字符串2跳回到0位置了,字符串1就要往后移一个了;
能够这样做完全是因为next表示了最长的相等前后缀,不明白的自己试几个例子,按照上面的流程走一遍就明白了。
代码
public static int indexOf(String a, String b) {
if (a == null || b == null || a.length() < b.length() || b.length() < 1) {
return -1;
}
//下文会给出next数组的求法
int[] next = next(b);
int i1 = 0, i2 = 0;
while (i1 < a.length() && i2 < b.length()) {
if (a.charAt(i1) == b.charAt(i2)) {
i1++;
i2++;
} else if (i2 == 0) {
i1++;
} else {
i2 = next[i2];
}
}
return i2 == b.length() ? i1 - i2 : -1;
}
这里不考虑next的求解的话,时间复杂度就是由这个循环决定的。
我们来看下这个循环到底走了多少次:
来看这两个值,v1 = i1
和v2 = i1-i2
,v1和v2的最大值显然都是n = a.length
;
循环里面是一个分支结构:
- if分支:v1增加,v2不变;
- else if分支:v1增加,v2增加;
- else分支:v1不变,v2增加;
所以如果循环次数超过了2n次,v1或v2里面必定有一个会爆掉。
所以这个循环的时间复杂度是O(n)
.
如何求next数组
如果b[i] == b[next[i-1]]
next[i] = next[i-1] + 1
不然的话,next[i-1]就再往前跳即可,跟kmp类似;
代码如下:
private static int[] next(String b) {
if (b.length() == 1) {
return new int[] { -1 };
}
if (b.length() == 2) {
return new int[] { -1, 0 };
}
int[] result = new int[b.length()];
result[0] = -1;
result[1] = 0;
int i = 2, j = 0;
while (i < b.length()) {
if (b.charAt(i - 1) == b.charAt(j)) {
result[i++] = ++j;
} else if (j == 0) {
i++;
} else {
j = result[j];
}
}
return result;
}
可以看出这个while循环的次数不超过2*b.length
次,设b.length = m
,则时间复杂度为O(m)
.
综上所述,整个KMP算法的时间复杂度为O(m+n)
.