一、BF算法
学习KMP算法之前,我们需要先学会BF算法:
BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。
——百度百科
文字有点晦涩,举个例子吧:
假设字符串“ababcabcdabcde”为主串,字符串“abcd”为子串,现在需要查找子串是否在主串中出现,找到就返回主串中第一次匹配的第一个字符下标,找不到则返回-1。
i 和 j 分别表示主串和子串的字符下标,当 i 和 j 位置的字符相同时,i ++,j ++
即:
↓
当i = 2 , j = 2 时,i 和 j 位置对应的字符不同,那么 j 就需要回到0下标的位置,而 i 需要回到起始位置+1的地方( i 从0下标开始的,起始位置此时是0)。
i 和 j 位置对应的字符还是不同,j 继续回退到0下标的位置,i 回到起始位置+1的地方(起始位置此时是1):
此时, i 和 j 位置的字符相同,i ++,j ++
↓
↓
对应位置的字符不同,j 回退到0下标,用代码表示就是 j = 0,而 i 回退到上次起始位置+1的地方,用代码表示就是:i - j + 1,因为 i 和 j 走的步数一样多,所以 i - j 就是回到刚才的起始位置,+1就是到起始位置的下一个下标。
当主串中找到与子串匹配的字符串时:
↓
此时 j 已经越界,循环结束,返回起始位置: i - j 。
如果 i 越界,说明主串中没有字符串可以和子串相匹配,循环结束,返回 -1。
转换为代码:
public static int BF(String str, String sub){ //str表示主串,sub表示子串
if(str == null || sub ==null){
return -1;//判断字符串是否为空
}
int lenStr = str.length();//主串长度
int lenSub = sub.length();//子串长度
if(lenStr == 0 || lenSub == 0){
return -1;//判断字符串长度是否为 0
}
int i = 0;//表示主串下标
int j = 0;//表示子串下标
while (i < lenStr && j < lenSub){
if(str.charAt(i) == sub.charAt(j)){ //比较
i++;
j++;
}else { //回退
i = i - j + 1;
j = 0;
}
}
if(j >= lenSub){
return i - j;//找到了
}
return -1;//没找到
}
二、KMP算法
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特--莫里斯--普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。
——百度百科
与BF算法的时间复杂度相比,KMP算法显然更加高效,因为KMP算法和BF算法的最大区别就在于:KMP算法中,如果 i 和 j 的字符不同,主串的下标 i 不会回退,子串的下标 j 也不一定会回退到0下标位置。
为什么主串的下标 i 不回退? 举个例子:
此时 i 下标的字符 和 j 下标的不同,i 回退到1下标位置是没有必要的, 因为它和 j 在0位置的下标还是不一样的。
如果 i 回退到1下标的位置,那么下一次它还是会到2下标的位置,所以不如不回退。
而 j 不一定回退到0下标的位置,那么 j 的应该回退到什么位置呢?
KMP算法的精髓就是next[ ]数组:next [ j ] = k,来表示不同的j对应的k值,这个k就是回退的下标位置。
举个栗子:
假设子串的2下标的字符和主串的不同,那么 j 可能回退到 1 下标, 如果在5下标匹配失败,那么可能会回退到 3 下标,如果在6下标匹配失败,可能会回退到 0 下标......
也就是说,子串中的每个字符的下标都有一个对应的 k 值,用来记录该下标字符如果匹配失败,它应该回退的位置;所以next[ ]数组长度和子串长度相同。
求 k 值规则:
1、找到匹配成功部分的两个相等的真子串(不包含本身),一个以0下标字符开始,另一个以 j - 1下标字符结尾的串。
2、next[ ]数组的0下标默认是-1 , 1下标默认是0 。
求 k 值的规则看不懂没关系,举个栗子就懂了:
next[ ]数组0下标和1下标的 k 值默认是-1 和 0,现在我们要计算next[ ]数组中的 2 下标,也就是 j 下标的 k 值, 那么就需要在子串中查找是否有两个相同的以0下标字符开始,j-1下标字符结束的字符串,即以 a 开始,以 b 结束的字符串,显然是没有的,那么next[ ]数组对应的2下标的 k 值就是0。
当 j = 3时,就需要找有没有两个相同的以0下标字符(a)开始,2下标字符(c)结束的字符串,也是没有,那么对应的next[ ]数组3下标的 k 值就是0。
当 j = 4时,找有没有两个相同的以0下标字符(a)开始,3下标字符(a)结束的字符串,这次我们就找到了"a",这个字符串的长度是1,那么对应的next[ ]数组4下标的 k 值就是1。
当 j = 5时,找有没有两个相同的以0下标字符(a)开始,4下标字符(b)结束的字符串,有!"ab",这个字符串的长度是2,那么对应的next[ ]数组4下标的 k 值就是2。
现在,我们已经求得这个子串所对应的next[ ]数组,当它和主串进行比较时,可以发现,当i = 5,
j = 5时,对应的字符不同,此时 i 不变,j 返回它对应的next[ j ]数组的值——2下标位置。
↓
↓
这就是整个KMP算法的逻辑,最主要的部分就是求出子串对应的next[ ]数组,这里给大家出两个求next[ ]数组的练习:
练习1:求字符串"a b a b c a b c d a b c d e"对应的next[ ]数组。
练习2:求字符串"a b c a b c a b c a b c d a b c d e"对应的next[ ]数组。
参考答案:
练习1:-1 0 0 1 2 0 1 2 0 0 1 2 0 0
练习2:-1 0 0 0 1 2 3 4 5 6 7 8 9 0 1 2 3 0
注意:
(1)
这样也是可以的,两个字符串可以有重复的部分。
(2)
这种计算方式是错误的。
第一个字符串必须是从0下标开始;
第二个字符串必须在 j - 1下标结束。
现在我们会手动计算next[]数组了,但是怎么把它转换成代码呢?
假设next[ j ] = k , 把字符串先看成一个字符数组 p :
如果p [ j ] != p [ k ]呢?
以上可以推导出:如果next[ j ] = k,那么next[ j + 1] = k + 1;否则k = next [ k ] 。
接下来我们就可以用代码写出整个KMP算法了:
public static int KMP(String str, String sub, int pos){
if(str == null || sub == null){
return -1;
}
int lenStr = str.length();
int lenSub = sub.length();
if(lenStr == 0 || lenSub == 0){
return -1;
}
if(pos < 0 || pos >= lenStr){
return -1;
}
int i = pos;//pos表示从指定下标开始向后查找
int j = 0;
int[] next = new int[lenSub];
setNext(next,sub,lenSub);//初始化next数组
while (i<lenStr && j < lenSub){
if(j == -1 || str.charAt(i)==sub.charAt(j)){
i++;
j++;
}else {
j = next[j];
}
}
if(j >= lenSub){
return i - j;
}
return -1;
}
public static void setNext(int[] next, String sub, int lenSub){
next[0] = -1;
next[1] = 0;
int j = 2;//从2下标开始设置,
int k = next[1];//k是j的前一项的k值
while (j < lenSub){
if(k == -1 || sub.charAt(j) == sub.charAt(k)){
next[j] = k + 1;
j++;
}else {
k = next[k];
}
}
}
next[ ]数组的优化:
假设有这样一个字符串"a a a a a a a a a b",
它的next[ ]数组是-1,0,1,2,3,4,5,6,7,8
假设在5下标匹配失败了,那么它回退位置的字符还是a,还是匹配失败,还需要一步一步地回退,效率很低,所以我们需要对next[ ]数组进行优化,计算优化之后的nextVal[ ]数组。
nextVal[ ]数组求法很简单,如果当前下标 i 回退位置的字符,正好和当前下标 i 对应的字符相同,
那么nextVal[ i ] = next[ next[ i ] ]
如果不同,则nextVal[ i ] = next[ i ]
转换为代码:
public static void setNextVal(int[] nextVal,int[] next, String sub){
nextVal[0] = -1;
nextVal[1] = 0;
for (int i = 2; i < sub.length(); i++) {
if(sub.charAt(i) == sub.charAt(next[i])){
nextVal[i] = next[next[i]];
}else {
nextVal[i] = next[i];
}
}
}
优化后的KMP算法的代码:
public static int KMP(String str, String sub, int pos){
if(str == null || sub == null){
return -1;
}
int lenStr = str.length();
int lenSub = sub.length();
if(lenStr == 0 || lenSub == 0){
return -1;
}
if(pos < 0 || pos >= lenStr){
return -1;
}
int i = pos;//pos表示从指定下标开始向后查找
int j = 0;
int[] next = new int[lenSub];
setNext(next,sub,lenSub);//初始化next数组
int[] nextVal = new int[lenSub];
setNextVal(nextVal,next,sub);
while (i<lenStr && j < lenSub){
if(j == -1 || str.charAt(i)==sub.charAt(j)){
i++;
j++;
}else {
j = nextVal[j];//j回退到nextVal数组中j下标的位置
}
}
if(j >= lenSub){
return i - j;
}
return -1;
}
public static void setNext(int[] next, String sub, int lenSub){
next[0] = -1;
next[1] = 0;
int j = 2;//从2下标开始设置,
int k = next[1];//k是j的前一项的k值
while (j < lenSub){
if(k == -1 || sub.charAt(j) == sub.charAt(k)){
next[j] = k + 1;
j++;
}else {
k = next[k];
}
}
}
public static void setNextVal(int[] nextVal,int[] next, String sub){
nextVal[0] = -1;
nextVal[1] = 0;
for (int i = 2; i < sub.length(); i++) {
if(sub.charAt(i) == sub.charAt(next[i])){
nextVal[i] = next[next[i]];
}else {
nextVal[i] = next[i];
}
}
}
不对的地方欢迎大佬指正~