目录
前言
本章节是在intelliJ IDEA集成开发环境测试的结果,内容多适用于学习Java方向数据结构的学者。
读前须知
以下算法解决的问题为:在一个主串中寻找一个指定的字串
一、BF算法(暴力算法)
Ⅰ、概念
BF算法的时间复杂度:O(m*n)
原理:BF算法(Brute-Force)也叫暴力算法,BF算法的查找逻辑是用两个指针分别指向主串和子串的第一个字符,如果字符相同,那么 判断后面的字符是否相同(进入同步判断,此时 i 下标可以被标记为 k),如果相同则继续进行同步判断(i++,j++),直到子串全部判断完毕,k下标即为子串在主串中的位置。如果字符不相同,那么 同步判断结束 ,i 回退到 k+1 下标继续判断主串,直到这种判断结束。如下图:
这里为了更容易理解所以取了一个k值记录同步判断的起始位置,实际上k值可以通过 i 和
j 的运算得到:k = i - j;
Ⅱ、代码实现
//返回主串中子串所在的第一个字符下标,没找到则返回-1
public static int bfFun(String s,String son){
if(s == null || son == null) return -1;
if(s.length() < son.length()) return -1;
if(s.length() == 0 || son.length() == 0) return -1;
int i = 0;
int j = 0;
//终止条件①:当主串遍历完毕时还没找到,此时跳出循环j < son.length()
//终止条件②:当子串遍历完毕,证明已经找到,此时j >= son.length()
while(i < s.length() && j < son.length()){
//判断是否进入同步判断
if(s.charAt(i) == son.charAt(j)){
i++;
j++;
}else{
i = i - j + 1;
j = 0;
}
}
if(j >= son.length()){
return i - j;
}
return -1;
}
二、KMP算法
Ⅰ、概念
KMP算法的时间复杂度为:O(m + n)
原理:KMP算法是由克努特(Knuth)、莫里斯(Morris)、普拉特(Pratt)共同设计实现的,所以由他们的每个名字首写得来。KMP的算法核心在于相对于BF算法,主串 i 下标不用回退,而是利用一个next数组来确定下一个同步判断的 j 下标。
举个例子:为方便理解,以下例子中 k 为同步判断的起始位置
上述例子不理解?请看下面更长的例子
为什么让 j 回退到指定位置, i 就不用回退:因为同步判断中k ~ i 中间的字符顺序是不变的,第一次同步判断失败时,第二次同步判断开始的起始位置可能会与第一次同步判断重叠一部分,而重叠的那一部分就能决定 j 的位置,并且重叠后第一次同步判断失败后 i 下标不变时,第二次同步判断只需要改变重复部分的长度 j 。需要注意的是:重复的部分长度可能为0。
Ⅱ、next数组
概念:next数组是记录字串与主串匹配失败时,字串 j 回退到到哪一个下标。
举个例子:由上面的例子
Ⅲ、next数组的求法
next数组是建立在子串中的,所以此时我们可以忽略主串。
next数组公式:从子串0号下标字符开始,到 j - 1号字符下标结束,寻找子串中 j 之前有没有相同的子串。有的话则 j 下标为相同子串的长度,没有则为0。
默认规则:next数组0号下标默认为-1,1号下标默认为0。一号下表为什么为-1请看下面
特殊性质:next数组中的数值只能按递增速率为1逐渐递增,不存在由1跳到3或者更高的情况。
举个例子:
这种计算仅用于书面计算,但是使用代码的话我们如何知道 j 下标前面是否复合公式的子串呢?
以下p数组为主子串,j 为遍历主子串的标记。
计算规则:
定义一个k下标记录上一次符合规则的子串长度(初始值为0),
此时p[k] 等于上一次子串的最后一个字符,
接下来判断主子串p[ j - 1 ] 是否等于上一次子串的后面一个字符,
如果相等则证明一定有相同的子串,且子串长度为上一次子串 + 1(k + 1)然后进入下一次
判断(j++)
如果不相等则进入主子串的子串中寻找有没有更小的子串符合规则:
现判断的子串中 j 前面没有符合规则的相同子串,
此时不进入下一次判断,使 k 指向上一次判断的next[k]的位置再去寻找(相当于找主
子串的子串的子串是否符合规则,因为在 j 之前的元素next数组都已经找好了的,
所以此时只需要在当前不符合规则的子串寻找有没有更小的子串符合规则),即将k 指
向上一次判断的next[k]--(k = next[k]) 此时 j 不+ 1。
如果没找到那么k最终等于next[0],即k = -1,进入next数组赋值 next[ j ] = 0 ,表示没
有相同子串。进入下一次判断(j++)
如果找到,则k 等于最终子串的回退下标。进入下一次判断(j++)
举几个例子:
Ⅳ、代码实现
public static int kmpFun(String s, String son){
int sLen = s.length();
int sonLen = son.length();
if(s == null || son == null) return -1;
if(sLen < sonLen) return -1;
if(sLen == 0 || sonLen == 0) return -1;
int[] next = new int[sonLen];
//获取next数组
getNext(son,next);
int i = 0;
int j = 0;
//i 遍历主串,j 遍历子串
while(i < sLen && j < sonLen){
//当没找到子串j回退到next指定下标
//找到子串则继续,直到i走完或者j走完
if(j == -1 ||s.charAt(i) == son.charAt(j)){
i++;
j++;
}else{
j = next[j];
}
}
//如果在主串中找到子串,此时j >= 子串长度
if(j >= sonLen){
return i - j;
}
return -1;
}
public static void getNext(String son, int[] next) {
//next数组前两个数字为 -1 0
next[0] = -1;
next[1] = 0;
//从2号下标开始给next数组赋值
int i = 2;
//k 为上一个子串最后一个字符的位置
int k = 0;
while(i < son.length()){
//循环找相同的子串
//找到给next数组赋值,并且进入下一次寻找(i++)
//没找到进入子串的子串继续查找,直到找到没有符合规则的子串,此时k = -1,进入下一次查找(i++)
if(k == -1 || son.charAt(i - 1) == son.charAt(k)){
next[i] = k + 1;
k++;
i++;
}else{
//进入子串中的子串继续查找
k = next[k];
}
}
}