介绍:
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n) 。
算法:
1 能解决什么问题
解决字符串匹配问题,有一个字符串A ,有个字符串B,问A是否包含B,如果包含返回A串与B串匹配开头的数组下标,如果不匹配则返回-1
A:abcabcdabce
B:abce
我们可以看到,A串的最后四个字符是和B串一样的,所以是可以匹配上的,但如果写代码逻辑,我们需要写两层循环,首先循环A的第一个和B的第一个进行比较,如果匹配上了,那A的第二个和B的第二个去继续匹配,直到B都匹配上,如果没匹配上,则是再从A的第二个字符和B的第一个字符再进行比较,直到循环匹配上或A的所有字符都比较完,停止循环。
这个基本的算法,时间复杂度是O(m*n),如果遇到很大的串,就会很慢,而KMP可以将时间复杂度降到O(m+n),这里m是指A串的长度,n是指B串的长度。
2算法引入
A:abcabcdabce
B:abce
还是这个串,我们用眼睛直接去做比对,来探寻一下,大脑是怎么匹配的。
首先,我先看到了第一个字符都是a,然后我继续看到了开头都是abc,发现匹配上了3个,就差最后一个了,我再看A串第四个竟然是a不是e,这里难免有点小沮丧。
但是我不会从A串的b开始匹配,因为我之间都比较过了嘛,abc中间的bc是不能再匹配上的,很明显,我从A串的四个会开始匹配,这个时候我有看到了一个abc 这个 abcabcdabce,然后发现结尾又是个d,哎,只能再匹配了,但我也不会从d匹配,因为B串开头是a嘛,然后从d后面开始匹配,最后匹配到了abce
从这个过程可以看出,我通过之前的比较,脑子记了一些东西,得出,如果按顺序继续暴力的去匹配,肯定匹配不上的结论,从而减少了匹配的次数,更快的完成了匹配。
KMP就是这样,他通过一些数据记录,从而避免了多余无效的比较,并且还能他给出当前最能匹配多长的串下标是哪个,从而继续匹配。
3 算法逻辑
算法主体分为2部分,1 是生成next数组 2是匹配
1 next数组生成
next数组,这个是由于字符串B进行生成的,是匹配过程中的工具,我们先看下如何生成next数组
A:abcabcabce
B:abcabce
需要新建一个next数组 长度和B串的长度一样,然后算出数据如下
a | b | c | a | b | c | e |
-1 | 0 | 0 | 0 | 1 | 2 | 3 |
说下 这个数字是怎么算的
abcabce e字符前面 abcabce 是一样的,代表前面有3个一样的 所以e下面是3
再多几个例子
aab 分别是-1 0 1 因为a和a对称
aaab 分别是-1 0 1 2 因为a和a对称 aa 和 aa对称,
aaaba 分别是-1 0 1 2 0因为a和a对称 aa 和 aa对称,最后一个a前面就没对称的了
再说咱们的字符B是怎么算的
第一个字符 和第二个字符 默认 -1 0
第三个字符c 看 第一个字符a和第二个字符b 判断不是对称
第四个字符a 首先第三个字符c下面的数据是0,第一个字符a和第二个字符c判断不对称,所以写0
第五个字符b 首先第四个字符a下面是0,第五个字符开始对比 第四个字符和第一个字符,一样,所以写1
第六个字符c 先看第五个字符b下面是1,所以下标1代表第二个字符,拿第二个字符和第五个字符比,一样,所以第五字符值+1 = 2 ,第六个字符写2
第七个字符e先看第六个字符c下面是2,所以下标2代表第三个字符,拿第三个字符和第六个字符比,一样,所以第六字符值+1 = 3 ,第七个字符写3
2 匹配
A:abcabcabce
B:abcabce
a | b | c | a | b | c | e |
-1 | 0 | 0 | 0 | 1 | 2 | 3 |
循环匹配,条件
1 循环A B 串进行匹配,直到匹配到了,或者匹配到A最后一个字符结束
2 进行一次匹配,我们发现按照顺序匹配A串和B串能匹配到第六个字符,当第七个字符a和e匹配的时候是匹配不上的,这个时候就要用到next数组了,e下面是三,所以 B下标匹配调到下标3的位置也就是第四个字符a,和A串的第七个字符a匹配,匹配上了,然后继续
A:abcabcabce
B:abcabce
3 大家发现,利用next串因为标注了B串对称的下标,又因为A串匹配到的和B串当前位置的之前都一样,所以根据next数组,直接就能跳过之前的匹配,减少了很多无用的比较。
4 假设B的第四个字符a和A的第七个字符是匹配不上的,那就要再看next数组,第四个字符对应0,所以就跳到0,也就是第一个字符再和A的第七个字符比较。
代码:
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();//x是str1 下标 y是str2下标int x = 0;int y = 0;// O(M) m <= n 获取next数组int[] next = getNextArray(str2);// O(N) 循环知道x 或者 y 超出 字符串最大长度 匹配结束,这里x条件跳出说明str1已经都比较过了,y跳出说明已经都匹配上了while (x < str1.length && y < str2.length) {if (str1[x] == str2[y]) {//如果匹配上了x 和 y都加1x++;y++;} else if (next[y] == -1) { // y == 0//没匹配上,但是y已经是0了, y0的位置next数组是一定等于-1的x++;} else {//如果没匹配上,获取next数组中的值,根据值的下标继续用当前x位置和获取到的Y位置进行比较y = next[y];}}return y == str2.length ? x - y : -1;}public static int[] getNextArray(char[] str2) {if (str2.length == 1) {return new int[] { -1 };}int[] next = new int[str2.length];next[0] = -1;next[1] = 0;int i = 2; // 目前在哪个位置上求next数组的值int cn = 0; // 这个记录者着之前能匹配到最大的前串尾巴,当前是哪个位置的值再和i-1位置的字符比较while (i < next.length) {if (str2[i - 1] == str2[cn]) {// 配成功的时候 把cn+1后放到i位置,然后i++next[i++] = ++cn;} else if (cn > 0) {//匹配没成功,继续匹配比他小的可匹配串cn = next[cn];} else {//否则cn=0说明没有课匹配的了 直接给i赋值0next[i++] = 0;}}return next;}// for testpublic static String getRandomString(int possibilities, int size) {char[] ans = new char[(int) (Math.random() * size) + 1];for (int i = 0; i < ans.length; i++) {ans[i] = (char) ((int) (Math.random() * possibilities) + 'a');}return String.valueOf(ans);}public static void main(String[] args) {int possibilities = 5;int strSize = 20;int matchSize = 5;int testTimes = 5000000;System.out.println("test begin");for (int i = 0; i < testTimes; i++) {String str = getRandomString(possibilities, strSize);String match = getRandomString(possibilities, matchSize);if (getIndexOf(str, match) != str.indexOf(match)) {System.out.println("Oops!");}}System.out.println("test finish");}}