KMP算法详解(保姆级别)
这东西没那么难理解,看别人的文章写的有点啰嗦了
自己会的东西不算会,能给别人讲明白才算会
背景
- 给你两个字符串,问你他们是否具有包含关系(连续的)!大部分都会首先想到暴力解法,一个个比较!直到找到比对上的!但是如果是这两个:
aaaaaaab
和aaaaaaac
就很操蛋了(O(n*m))!里面包含很多重复比较的过程,如何设计一种算法来减少这种重复很有必要!
这就是KMP研究的领域(加速BUFF)!不过想要彻底弄明白KMP;还得先搞懂一个最大前缀数组
最大前缀数组(最长前缀&&最长后缀)
假定一个字符K,我们给它赋予一个信息,
那么这个next数组放什么东西呢?------最大相同前后缀!举个例子
- abbcabbk------对于K前面的字符串:abbcabb;相同的前后缀有哪些?(前n个和后n个相同)显然:abb,所以next={a,b,b};注意:不包括自身如abbcabb,如果有多个!选最长的!实际上我们只需要长度信息就可以了!就像next[k]=3;
比方说:字符串“ababc”的next={-1,0,0,1,2};第一个a前面没有元素,则为-1;
在代码中如何体现呢?很简单啦!设计个小小的算法!
具体如何实现,你就先把它当作一个可用的API把KMP解决后再回过头来研究它如何设计!
给定两个字符串 判断str1中是否包含str2字符串!
str1="abbtcfabbtkadd
······"
str2="abbtcfabbtu
······"str2中的每个字符都有next信息!
str1 | a | b | b | t | c | f | a | b | b | t | k | a | ··· |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
str2 | a | b | b | t | c | f | a | b | b | t | u | v | ··· |
step2:str1 | × | × | × | × | × | × | a | b | b | t | k | a | |
str2 | a | b | b | t | c | ||||||||
step3:str1 | × | × | × | × | × | × | × | × | × | × | × | a | |
str2 | a | b | b | t | c | f | a | b | b | t | u | v | ··· |
-
第一步:传统比较----str1[i] 比较 str2[i],比较到最后一个元素–>k != u 比对失败;跳到下一次比较!
-
第二步:传统是从str[1]重复第一步!显然 太慢了,这时候就需要KMP闪亮登场!拿到第一步比对的最后一个元素!—u(其中包含其最长前缀数组**【abbt】**),这一次比对str1不用回到str1[1],而是就停在原地----->k;那么str2从哪里开始和他比对呢?
仔细看看k前面的元素:abbt,这是什么?这不就是str2中u的前缀数组吗?所以str2就跳到最长前缀abbt的后一个开始比对!c
step2:str1 × × × × × × a b b t k a str2 a b b t c -
第三步:显然:k != c;比对失败;重复第二步!c的前缀数组显然为空了,也就是长度为0;str2表示自己已经没有选择的余地了!只能让str1选了!str1只能向后移动:选k的下一个元素和str2重复一二三步!
step3 | × | × | × | × | × | × | × | × | × | × | × | a | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
a | b | b | t | c | f | a | b | b | t | u | v | ··· |
理论存在,动手敲代码!这里使用Java实现!
- 数组比对,索引必不可少!首先定义i1、i2两个索引对应str1、str2;
- 拿到str2每个元素的最大前缀数的数组:
getNextArray(str2);
- 循环判断,条件是i1、i2不越界:
i1 < str1.length() && i2 < str2.length()
- 三种情况对应上面三步;
- 1、比对上了:i1++;i2++;
- 2、比对不上;i2跳到最大前缀后一个:
i2 = next[i2];
- 3、2能不能跳还得看有没有前缀:没有了就不能跳了也就是
i2 == 0
,这时候就得让i1++
public static int kmp(String str1,String str2){
//终止条件
if (str1 == null || str2 == null || str1.length() < str2.length()){
return -1;
}
char[] str11 = str1.toCharArray();
char[] str22 = str2.toCharArray();
int i1 = 0;
int i2 = 0;
int[] next = getNextArray(str22);
//循环条件,i1\i2不可越界
while(i1 < str1.length() && i2 < str2.length()){
if (str11[i1] == str22[i2]){
i1++;
i2++;
}else if (i2 == 0){//没有前缀可以跳了
//就让i1向后挪动
i1++;
}else {
//让i2移动到前缀后面一位,也就是next[i2]
i2 = next[i2];
}
}
//返回条件:i2是不是到最后一个元素了?
return i2 == str22.length ? i1 - i2 : -1;
}
最长前缀树算法
我自己思考半天写出一个最初版本:
使用的是暴力解法,每个字符都得找出他的前缀长度,这得需要一个for循环!对于这个元素,找它最大可能的前缀长度,就先求最长的时候!max = i(当前元素数组下标) - 1,min = 1;这又是一个循环;这里使用while,定义一个变量x从1到i-1递增!
最后判断这个长度对应的前后缀是否相等,又是一个for循环!循环区域从0~i-x-1;前缀和后缀元素不相等就这一轮报废!到下一轮,X+=1;如果全部都相等了(最后一个也相等),就不用进行下一轮了,因为这一轮的结果就是最大的!让x = i跳出while循环进行下一个元素的计算!
//找到该字符串的最大相等前后缀
private static int[] getNextArray(char[] str22) {
if (str22.length <= 1){
return new int[]{-1};
}
//判断前n和后n是否相等
int[] arr = new int[str22.length];
//数组的第一第二个一定是-1和0;不会变的
arr[0] = -1;
arr[1] = 0;
//遍历剩下的元素!
for (int i = 2; i < str22.length; i++) {
int x = 1;//第一轮···对应最大长度
while (x <= i-1){//总的论数:i-1
//这一轮的元素长度
for (int j = 0;j<=i-x-1;j++){
if (str22[j] != str22[j+x]){
x += 1;
break;
}else if (str22[i-x-1] == str22[i-1]){//全部相等,找到最大长度
arr[i] = i-x;
x = i;
}
}
}
}
return arr;
}
可以说是很容易理解!但是!自己写的永远不是最佳方案!下面介绍更高级的解法!
首先:next数组的第一第二元素肯定是规定好了的-1和0这毋庸置疑!那么我们到第三个元素了的时候能不能利用第二个元素的信息呢?
可以看到,我们直接利用前一个元素的信息,在它的基础上进行比较!如果前一个位置str[i-1] == str[next[i-1]], ==str[next[i-1]]为i-1的最长前缀的下一个元素,与最长后缀的下一个元素(i-1)str[i-1]==进行比较!那么就能推出:next[i] = next[i-1] + 1;
如果不等于呢?把i-1变成next[next[i-1]], 用它的信息(也就是不等与的这个位置)重来一次!
代码:
private static int[] getNextArray2(char[] str22) {
if (str22.length <= 1){
return new int[]{-1};
}
//判断前n和后n是否相等
int[] arr = new int[str22.length];
arr[0] = -1;
arr[1] = 0;
//next数组的位置
int i = 2;
//需要一个变量进行跳转;
int ct = 0;
while(i < arr.length){
//如果前一个元素等于最长前缀的后一个
if (str22[i-1] == str22[ct]){
arr[i++] = ++ct;
}else if (ct > 0){
ct = arr[ct];
}else {
arr[i++] = 0;
}
}
return arr;
}
三个条件分支是不是有点像KMP的三个条件分支,所以KMP难得不是它本身,而是计算前缀数组信息