KMP算法
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n) [1] 。
最长公共前缀
了解KMP算法之前先要了解一下什么是最长公共前缀
下面有一个字符串
"a b c a b a"
然后这里有一组数组跟上面的字符串对应
[-1 0 0 0 1 2]
跳过前面2个字符串因为前面两个字符串没有公共前缀的概念
because:当字符串长度为2的时候没有取最长公共前缀的意义,我们能够通过2个字符的比较得出结论。
下面来解释一下数组的含义
前面说过了2个字符串没有公共前缀的概率
所以这里我们人为规定数组的第一个值和第二个值分别为-1和0
数组变化
[-1 0]
从字符串下标为2的前面开始观察,当字符串下标为2的时候。
我们从字符串的最前面取1个值a(也就是下标为0的那个元素),从下标为2的前一个2-1也就是下标为1的字符取出来为b
因为a并不等于b,所以我们给字符串下标为2的数字设置为0。
表示字符串下标为2的位置的之前的字符串,存在的最长公共前缀为0
数组变化
[-1 0 0]
这个时候重复前面步骤
从字符串下标为3的前面开始观察,当字符串下标为3的时候。
我们从字符串的最前面取1个值a(也就是下标为0的那个元素),从下标为3的前一个3-1也就是下标为2的字符取出来为c
因为a并不等于c,所以我们给字符串下标为3的数字设置为0。
表示字符串下标为3的位置的之前的字符串,存在的最长公共前缀为0
数组变化
[-1 0 0 0]
字符串还没有观察完继续上述步骤
这个时候我们来到了字符串下标为4的位置
我们从字符串的最前面取1个值a(也就是下标为0的那个元素),从下标为4的前一个4-1也就是下标为3的字符取出来为a
a等于a这个时候终于相等了,所以这个时候给下标为4的的数字设置为1.
表示字符串下标为4的位置的之前的字符串,存在的最长公共前缀为1
数组变化
[-1 0 0 1]
这个时候来到了最后一步了
这个时候我们来到了字符串下标为5的位置
这个时候我们只需要比较字符串的之前的比较过的前面一个值开始比较,因为a和a前面已经比较过了。
所以我们只需要比较字符串下标为1的值和下标为5的前一个也就是5-1的值
因为b=b,所以我们这里只需要取出前一个数组前面一个的值进行加1
数组变化
[-1 0 0 1 2]
特殊情况
这里还有一种特殊情况就是当字符串不相等的情况下这个时候需要跳转比较了。
根据上面的规律这个时候下标为6的a应该和下标为3的值c进行比较
但是a是不等于c的,
所以a这里的指针不动,直接跳转到下标为2的位置的前面的最长公共前缀元素开始比较,取出c位置下标的值,
通过这个索引取出这个下标的字符串和上面指针指向的a开始重新比较。
这里为什么需要跳转我的理解是,因为在指针a的这个位置和c值不相等了,所以公共前缀到这里就已经断了。
所以,需要找到c位置开始之前的公共前缀。在这里需要解释一下字符串下面的数组的值。
这里数组的值可以理解为2种含义,第一个含义就是从这个位置出发前面取出和下标一样个数的公共前缀有几个
第二个含义就是该下标从字符从下标0出发到这个下标的值的范围里面的字符串和第一个含义里面范围的字符串是一样的。
就是上面圈出来的”ab“和”ab“这两个值。所以当a不等于c的时候,从下标数组[2]取出来值等于0
然后取出字符串数组[0]的值开始和指针a开始比较来重新计算一个最长公共前缀。
下面贴一下代码——师承左神
public int[] getKmpArray(char[] needleArray) {
int length = needleArray.length;
if (length < 2) {
return new int[]{-1};
}
int[] newArray = new int[length];
newArray[0] = -1;
int a = 2;
int b = 0;
while (a < length) {
if (needleArray[a - 1] == needleArray[b]) {
newArray[a++] = ++b;
} else if (b > 0) {
b = newArray[b];
} else {
a++;
}
}
return newArray;
}
求出这个最长公共前缀之后,我们可以开始干大事了
利用最长公共前缀加速匹配字符串
下面我们来看一下kmp的原理
这里便于观看我先用空格隔开字符串
String haystack="q w e r t r s q w e r t l q w e r t"
String needle="q w e r t r s q w e r t k"
之前的暴力匹配是for循环第一个字符串匹配到最后l的时候,发现和k不匹配,这个时候会重新从w开始遍历开始比较
这个时候时间复杂度就会大大的提高了。
而KMP干了什么一个事,KMP会帮助我们跳转到下面这么一个情况
会直接把我们需要去进行匹配的字符串的l指向r这么一个位置。
这里会有一个疑问?为什么我要指向这个位置?
大家还记得,前面我们求了最长公共前缀这一个数组对吧
仔细观察上面这个字符串我们会发现一个规律
这里的qwert在下面出现了两次,上面出现了一次
我们可以发现qwert和qwert是相等的了。所以我们直接把指针l指向r这个位置让他们来进行比较。
这里的疑问是为什么我能直接把l指向r这个位置?我qwertl前面不会有字符串会跟下面要匹配的字符串能匹配的吗?
这里我们就需要利用反证法了,如果说qwert前面会有字符串跟下面的匹配,那么也就是说会产生一个比qwert更长的
公共前缀,但是我之前已经求出了K前面的最长公共前缀为qwert了它已经是我能找到的最长公共前缀了,所以这里的结论是不成立的。
所以怀疑失败,上面字符串qwertl
之前的字符串我们可以直接放弃不看了。因为根据上面的方我们已经知道了
因为前面部分的字符串是永远也不可能拼出我们要找的字符串的。
根据上面我们所求的数组我们可以得到K这里的值应该为5。所以上面的l应该指向r这个位置。
你可以看成有人直接把下面这个字符串往右边推了一下。
如果r和s不等,那么我们又对r的位置找一个最长公共前缀重复该操作。直到没有公共前缀
也就是当上面的l指向q的时候,已经不能在推了,那么这个时候只有对上面的字符串往下面取值。
换一个开头。
KMP核心代码
/**
* KMP算法
* @param haystack 目标字符串
* @param needle 出现的字符串
* @return int 字符串出现的索引(从0开始)
*/
public int strStr(String haystack, String needle) {
char[] haystackArray = haystack.toCharArray();
char[] needleArray = needle.toCharArray();
int[] kmpArray = getKmpArray(needleArray);
int haystackLength = haystackArray.length;
int needleLength = needleArray.length;
if (haystack == null || needle == null || needleLength < 1 || haystackLength < needleLength) {
return -1;
}
int haystackIndex = 0;
int needleIndex = 0;
//保证数组不越界
while (haystackIndex < haystackLength && needleIndex < needleLength) {
//如果都相同两个数组同时往后面移动并且自增
if (haystackArray[haystackIndex] == needleArray[needleIndex]) {
haystackIndex++;
needleIndex++;
//如果不相同有两种情况
//如果要比较的数组来到了0的位置,
// 因为前面KMP数组规定了数组的第一个值的下标为-1
//这里也可以写为needleIndex==0
} else if (kmpArray[needleIndex] == -1) {
haystackIndex++;
} else {
//跳转要比较的数组到公共前缀的位置
needleIndex = kmpArray[needleIndex];
}
}
//如果指针的位置到了最后一个元素返回-1
// 如果没有说明找到了匹配的元素,2个指针指向的位置相减等于要匹配的元素第一次出现的位置
return needleIndex == needleArray.length ? haystackIndex - needleIndex : -1;
}