1. KMP算法解决的是什么问题
用于查找字符串A中是否包含子字符串B。
2. 为什么要用KMP算法
寻找字符串B是否在字符串A中,最简单暴力的方法就是,以A的第一个字符(简称为A-0,下同)为开头,B-0为开头,依次往后比对字符串B。
比对完全相等,返回,比对不相等,
B回退到B-0,A回退到A-1,以A-1为开头,依次往后比对B
依次类推,直至匹配字符串B或者以字符串A的某个字符开头的字符串长度小于B的长度,结束比对。
假设字符串A,B的长度分别是n,m
上诉这种方法的时间复杂度为O(n*m)
而kmp算法,可以使时间复杂度达到O(n)
3.KMP算法是怎么实现的
其实原理跟暴力方法一样,也是要以某个字符为开头,依次往后比对,只不过在比对不相等时,处理方式有所优化,kmp算法不会将B直接回退到第一个字符,而是回退到B的某个特定字符,而A不会回退,直接保留在原地或者前进
流程实现:
a.先准备一个与字符串B等同长度的数组,称之为next数组,用于存储B所有字符位置之前的前缀和后缀相等的最长长度。
如图字符串B,index=2的位置,在它之前的字符串中,前缀和后缀相等的最长长度是1,即next[2]=1,index=3的位置长度为2,即next[3]=2。
b.开始比对
b.1 以A,B第一个字符开始比对,比对到不等的字符,如图,比对到index=5,字符不等,
b.2 此时A保留在index=5的位置,而B回退到next[5]值的位置,假设为3,
b.3 然后以此为对比点,往后开始对比,如果不等,B再回退到next[3]值的位置,假设为0,
b.4 然后以此为对比点,往后开始对比,如果不等,由于此时B无法再退了,所以此时A开始前进,即以A-6为对比点,与B开始往后比对,
b.5 依此类推,直至匹配到字符串B或者以字符串A的某个字符开头的字符串长度小于B的长度,结束比对。
实质原理:
上诉步骤中,有没有这个疑问:为啥在比对不等的情况下,A可以保留在index=5的位置(而不是回退到它原本应该的比对点A-1),B回退到next[5]值的位置(而不是回退到起点B-0),实质是什么?
实质一:
其实B回退到next[5]值的位置,实质是B回退到了起点B-0,A从A-5回退了next[5]值,也就是回退到了A-2,以A-2为对比点,往后开始对比,
但因为B-0~B-2与A-2~A-5这一段是相等的(因为B-5的next值为3,也就是前缀长度为3和后缀长度为3的字符相等,即B-0~B-2==B-2~B-4,而因为上诉b.1步骤是对比到index=5才出现字符不等,所以,B-2~B-4==A-2~A-4,所以B-0~B-2==A-2~A-4),所以省去了对比,直接从B-3,A-5开始对比。
实质二:
但为啥以A-0开头对比失败后,直接跳过了A-1,直接以A-2为对比点,为什么可以跳过A-1的对比点。其实也是证明以A-1开头找不出B。
这里采用反证法证明一下,我们不妨假设以A-1开头可以找出B,即A-1~A-7==B-0~B-6
那么也就是说A-1~A-4长度为4的字符串,等于以B-0开头同等长度的字符串,也就是B-0~B-3==A-1~A-4,
而又因为上诉b.1步骤是对比到index=5才出现字符不等,所以B-1~B-4==A-1~A-4,所以B-1~B-4==B-0~B-3,也就是B-5此时的前缀和后缀相等的最大长度为4,但我们b.2步骤说明了B-5的next值为3,互相矛盾,所以假设不成立,也就是以A-1开头找不出B,所以可以直接跳过A-1。
代码实现&注释解析(java):
public class KMP {
/**
* 主方法 查询字符串str中能否找出match字符串,如果找不出,返回-1,如果可以找出,返回匹配开始点
*
* @param str 字符串A
* @param match 字符串B
* @return
*/
public static int getMatchStrStartIndex(String str, String match) {
if (str == null || match == null || str.length() == 0 || match.length() == 0 || str.length() < match.length()) {
return -1;
}
char[] strArr = str.toCharArray();
char[] matchArr = match.toCharArray();
//生成next数组
int[] next = returnNextArr(matchArr);
//字符串A指针
int strIndex = 0;
//字符串B指针
int matchIndex = 0;
//当A、B任意一个指针超过字符串长度时,比对完毕,退出循环
while (strIndex < strArr.length && matchIndex < matchArr.length) {
if (strArr[strIndex] == matchArr[matchIndex]) {
//当A、B字符串字符相等,各自往后走一步
strIndex++;
matchIndex++;
} else if (matchIndex == 0) {
//当A、B字符串字符不等且B字符串指针为0时,退不了了,所以只能字符串A往后走
strIndex++;
} else {
//当A、B字符串字符不等且B字符串指针不为0时,A指针保持原地,B指针退回到next[B指针位置]值处
matchIndex = next[matchIndex];
}
}
//当字符串B指针大于等于字符串B长度,说明字符串A可以找出字符串B,返回匹配开始点,否则返回-1
return matchIndex >= matchArr.length ? strIndex - matchArr.length : -1;
}
/**
* next数组
* 数组含义:i位置之前,前缀和后缀相等的最大数量,如aaabaaabt,t所在位置值为4(aaab),而aaabaaab不算,因为跟t之前的字符串长度等长了
* <p>
* 人为规定0位置值为-1,1位置值为0
* <p>
* 数组创建思路:设x位置next数组值为5,求x+1位置next数组的值
* 1.判断x位置的字符是否与5位置的字符相等,如果相等,x+1位置next数组的值为5+1=6
* 2.如果不等,x位置跳到5的位置上,重复上面动作
* 3,如果x已经来到0位置了还不等,那x+1位置next数组的值为0
*
* @param match
* @return
*/
public static int[] returnNextArr(char[] match) {
if (match.length == 1) {
return new int[]{-1};
}
int[] result = new int[match.length];
result[0] = -1;
result[1] = 0;
int index = 2;
int preResult = 0;
while (index < result.length) {
if (match[preResult] == match[index - 1]) {
result[index++] = ++preResult;
} else if (preResult != 0) {
preResult = result[preResult];
} else {
result[index++] = 0;
}
}
return result;
}
// 验证测试使用
public 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);
//暴力方法
int i1 = str.indexOf(match);
//kmp算法
int matchStrStartIndex = getMatchStrStartIndex(str, match);
if (matchStrStartIndex != i1) {
System.out.println("no!");
System.out.println(str + "---" + match);
break;
}
}
System.out.println("test finish");
}
}