相信大家对于这两个算法一定都听过吧,由于博主刚学,所以目前觉得这两算法就是用于在主串中查找子串第一次出现的位置,然后返回这个下标,有什么不同的看法我会再慢慢去了解。接下来我将会慢慢讲一下这两个算法。
1.BF
要了解什么是KMP,我们需要首先了解什么是BF算法,BF(Brute Force)暴力算法,也是最能想到的方法。先假设主串为str , 子串为sub,思路是主串和子串同时都从0下标开始走,当遇到第一个不相等的时候,str回到之前出发的下一个下标(排除前一个),sub回到0下标,然后继续出发,如果子串走完,说明找到了。如果主串走完了,说明没有找到这样的子串。下面我来画一个图帮助理解
if(str == null || sub == null) {
return -1;
}
int lenstr = str.length();
int lensub = sub.length();
int i = 0; //遍历主串
int j = 0; //遍历子串
if(lenstr == 0 || lensub == 0) {
return -1;
}
首先定义变量,lenstr为主串长度,lensub为子串长度。排除字符串指向为空,或者字符串为空的情况;
while(i<lenstr && j<lensub) {
if(str.charAt(i) == sub.charAt(j)) {
i++;
j++;
} else {
i = i-j+1; //回到之前出发的下一个
j = 0;
}
}
用while来判断,如果出去了,说明找到了或者没有这样的子串
我们用str.ChaeAt查看下标的字符,如果相等就i++,j++;如果不相等就主串下标回到之前出发的下一个(这样就可以排除一个,逐步排除,直至走完主串),这里可能是唯一的难点,怎样回到出发前下标的下一个呢,因为i和j 一起走的,步数一样,所以i-j就回到起点了,这时候再加1,就到下一个下标了。而j每次回到起点就好了。当跳出循环时,我们需要去判断是因为哪个条件导致循环结束的。代码如下
if(j >= lensub) { //如果j遍历完,说明找到了子串;
return i-j;
} else { //如果i遍历完,说明主串没找到子串;
return -1;
}
如果 j大于等于了子串长度(当然不可能大于),说明j遍历完了,说明找到了这样的子串。这时候i返回到这一轮开始匹配的下标,如果不是这个条件,说明主串走完了,这时返回-1即可。
在BF中,我们可以看出,当i下标为1的时候起始没有必要去比较的,因为它属于前三个,已经走过一次了,所以剩下两个不会匹配上,可以从三下标开始的,为了减少算法次数,进而优化出了KMP
二.KMP
关于KMP,它的核心思路是i不会回退了,j回退,但是j呢也不是每次都回到起点,每次回退到哪个下标都不一定,所以我们需要去定义一个数组,用来存放当在这个位置不匹配时需要退到哪个下标。而KMP难点就在于每个下标对应数组下该存什么值。
正如这张图,当在5下标匹配不成功时,i不动,j后退,假设我们这个5下标下存的是2;
所以j退到2下标,然后去匹配i =5,j=2,由图可知,不相等,这时候,我们再去找2下标下存的数组值,假设存的是0,即i退到0下标,这时候再去匹配,i=5,j=0;发现匹配上了,这时候就i++,j++;然后发现找到这样的子串了,所以返回i-j(由上面BF可知)即可;难点即找到每个下标存什么值。在这里,我们会统一数组0下标放-1,1下标放0;后面的则需要去推敲每一个应该放什么。
规则是“找到匹配成功部分的两个相等的真子串(不包含本身)”一个以0下标开始,另一个以j-1下标结尾。
比如j=5,那么需要从0下标到j-1(4)下标去找两个相等真子串,这里我们可以看到0到4下标有两个相等真子串,即(ab)有两个,所以下标为存2;再比如j=4下标,从0-3开始找,这里只有0下标的a和3下标的a对上了,即(a)有一个,所以存1;再比如j=3;从0-2开始找,找不到连个相等的真子串,找不到就存0,以下类推。
j = 5 的图,可知为2;相等真子串为“ab” 所以该下标数组存2
j = 4的图,可知为1;相等真子串为“a” 所以该下标数组存1
当j = 3时候,找不到两个相等的子串 ,就存0
我们一起来练习一个,相信大家就会找了
当j = 11的时候,我们由图可知为8,所以该下标数组存8,
当 i = 7时,我们可知为4,所以该下标数组存4;
接下来我就不搞了,大家可以自己去试试把其他补全。(统一0下标为-1,1下标为0)所以答案是
-1 0 0 0 1 2 3 4 5 6 7 8 大家可以练习一下这个串的下标应该存什么
a b a b c a b c d a b c d e 答案为(-1 0 0 1 2 0 1 2 0 0 1 2 0 0)
这里有个小提示,如果后一位比前一位大,那么一定只增1,意思只能一次加1,如果一下子大多个数,就是数错了。
思想大概就是当匹配不上就回退到对应数组该下标存储的值的下标;然后与j匹配,如果匹配失败,重复上操作,继续退到该下标存储的值的下标去,就好比下图
接下来就是代码的实现了。
int lenstr = str.length();
int lensub = sub.length();
int i = 0; //遍历主串
int j = 0; //遍历子串
if(str == null || sub == null) return -1;
if(lenstr == 0 || lensub == 0) return -1;
同样的处理其他情况。
int[] next = new int[lensub]; //建立一个数组存放每个下标回退到哪
getNext(sub,next);
//定义一个方法传子串和数组,在方法里求得每个数组下标存的值
while(i<lenstr && j<lensub) {
if(str.charAt(i) == sub.charAt(j)) {
i++;
j++;
} else {
j = next[j]; //j回退到该下标对应值的下标
}
}
难点是方法的内部怎样实现,怎样去求得数组每个下标存的值,代码如下
public static void getNext(String sub,int[] next) {
{
next[0] = -1;
next[1] = 0;
int i = 2; //表示当前所求next数组的下标
int k = 0; //表示i前一项所需要退回的值
while(i < sub.length()) {
//当k == -1时 ,表示next[0]到j-1没找到符合真子串;
if(k == -1 || sub.charAt(i-1) == sub.charAt(k)) {
next[i] = k+1; //所以next[i] = -1+1 = 0;
k++; //所以下一个i前面的k=0,需要退回到0下标
i++;
} else {
k = next[k];
}
}
}
}
最后这个方法需要好好琢磨琢磨,这样一个KMP就解决好了,但是还有一个小问题,当子串长度为1的时候,会空指针异常,所以我用了个笨方法,当子串长度为1,我又调了次BF算法解决。全部代码如下
public static int KMP(String str,String sub) {
int lenstr = str.length();
int lensub = sub.length();
int i = 0; //遍历主串
int j = 0; //遍历子串
if(str == null || sub == null) return -1;
if(lenstr == 0 || lensub == 0) return -1;
if(lensub == 1) {
while(i<lenstr && j<lensub) {
if(str.charAt(i) == sub.charAt(j)) {
i++;
j++;
} else {
i++;
j = 0;
}
}
} else {
int[] next = new int[lensub];
getNext(sub,next);
while(i<lenstr && j<lensub) {
//如果第一个不相同,就会进入else,这个时候j会变成-1,然后进入if,i++就往后走,j++重回0,相当于还在0下标,反复
if( j == -1 ||str.charAt(i) == sub.charAt(j)) {
i++;
j++;
} else {
j = next[j];
}
}
}
if(j >= lensub) { //如果j遍历完,说明找到了子串;
return i-j;
} else { //如果i遍历完,说明主串没找到子串;
return -1;
}
}
public static void getNext(String sub,int[] next) {
next[0] = -1;
next[1] = 0;
int i = 2; //表示当前所求next数组的下标
int k = 0; //表示i前一项所需要退回的值
while(i < sub.length()) {
if((k == -1) || sub.charAt(i-1) == sub.charAt(k)) { //当k == -1时 ,表示next[0]到j-1没找到符合真子串;
next[i] = k+1; //所以next[i] = -1+1 = 0;
k++; //所以下一个i前面的k=0,需要退回到0下标
i++;
} else {
k = next[k];
}
}
}
public static void main(String[] args) {
String str = "abcabcabd";
String sub = "abd";
int ret = KMP(str,sub);
System.out.println(ret);
}