提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
提示:BF、KMP算法
主要是用来记录一个很重要的算法kmp,以及是怎么推过来的
BF算法实际上就是一种暴力算法,我们比较字符串,分为模式串和文本串,分别用 pat 表示模式串以及 txt 表示文本串,不管是BF算法还是KMP算法,我们都是在 txt 中查找子串 pat 的下标,如果存在,返回这个字串的起始索引,否则则返回-1。
作为暴力算法来说,比较的思路非常简单,其实就是分别确立一个下标i和一个下标j,来分别对应pat和txt,i和j对应的字符比较,如果都比较成功,那么i++, j++,如果比较不成功,那么j回溯到1,i回溯到i-j+2。
i-j+2是怎么来的呢?
因为i已经走过了j-1个字符都不可以,所以回溯到i-(j-1),但是i本字符也不可以,所以就要再往下挪一个,所以变成了i-(j-1)+1, 所以就是i-j+2.
代码如下(示例):
//BF算法
int index_BF(SString txt, SString pat, int pos){
//一般pos为0
int i = pos;
int j = 1;
//txt和pat的第0个都是字符串长度
while(i <= txt[0] && j <= pat[0]){
if(txt[i] == pat[j]){
i++;
j++;
} else {
i = i - j + 2;
j = 1;
}
}
if(j > pat[0]){
//这个时候全部匹配完了,但是i没有回溯,那么下标索引就要减去pat长度
//即为字串匹配索引
return i - pat[0];
} else {
return 0;
}
}
BF是一种暴力算法,虽然很直观,但是浪费了非常多的效率,比如:
txt:abaabaab
pat:ababc
每次比较完之后,j都需要回溯到第一个字符下标下,但其实,我们可以发现,pat中的aba与txt字符串abaabaab中的是有公共前缀的,所以当比较到那里的时候,其实不需要回溯到第一个字符下,而是直接回溯到aba中的最后一个a开始比较就可以了,然后判断txt中的下一个是否匹配来决定j是否回溯到第一个字符下。
那么由此我们可以得到,kmp算法其实与bf算法总体代码上没有太大的区别,唯一的地方在于当两者不匹配相同的时候
i是永远不会后退的,而j的回溯方式不同
所以,我们只需要更改BF算法代码中的不匹配的回溯部分以及新增一个回溯方式即可,其它部分相同。
对于回溯到哪里,我们使用next数组来进行记录,记录当到了index下标的时候,j移动next[index]的位置即可。
next的状态其实很像影子,跟在j的后面:
1、待j找到最长公共子串的时候的话就对从位置1到j-1构成的串中所出现的首尾相同的字串最大长度加1,然后把这个长度赋值给next[i],即next[i]=j,就是记一个长度,等到不行的时候挪动j个就又回到了当初的地方,相当于回到了影子在的地方
2、初始化的时候,next[1]=0,表示根本不进行字符比较,就算比较相同,因为这个时候i=1,j=0,其实和1相同,再往回就到了最开始了
3、当没有首尾相同的字串的时候next[j]=1,表示从pat的头部开始进行字符比较
也就是说,除了j=0以及pat[i] == pat[j]的情况是next[i]=j之外,其他都是j=next[j]进行挪动回溯
取决于当前状态以及下一个元素符号的状态
代码如下(示例):
//KMP算法
int index_KMP(SString txt, SString pat, int pos) {
//一般pos为0
int i = pos;
int j = 1;
//txt和pat的第0个都是字符串长度
while(i <= txt[0] && j <= pat[0]){
//注意BF算法这里没有0,因为这里next[j]可能为0
if(j == 0 || txt[i] == pat[j]){
i++;
j++;
} else {
//i不变,j后退
j = next[j];
}
}
//这个时候全部匹配完了,但是i没有回溯,那么下标索引就要减去pat长度
//即为字串匹配索引
if(j > pat[0]){
return i - pat[0];
} else {
return 0;
}
}
//使用pat构造next数组,因为有对next数组的更新,所以next[]使用了&来引用
//而且txt中的i是不会后退的,主要是观察j的回溯情况,j的回溯情况却决于当前的状态和下一个字符
//所以我们对pat构造next数组就达到了完成j回溯,从而比较后反馈给i
void get_next(SString pat, int & next[]){
int i = 1;
int j = 0;
//为什么要next[1] == 0 ?
int next[1] = 0;
//为什么是 < 而不是 <= ?
while(i < pat[0]){
//
if(j == 0 || pat[i] == pat[j]){
i++;
j++;
next[i] = j;
} else {
j = next[j];
}
}
}
next数组不考虑移动到的位置的数与当前位置数的关系,而nextval则更进一步,考虑到了,所以nextval是对next的深入,也是提高效率的一种改进。
nextval数组用来解决多个重复元素出现之后,子串移动太慢的问题,要知道next数组才可以知道nextval数组。
举例:
txt :aaabaaaab pat :aaaab
j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
pat | a | a | a | a | b |
next | 0 | 1 | 2 | 3 | 4 |
当txt出现失配的时候,那么j=next[j],即从原来的j=4变成了j=3 ,所以把j回溯到了pat[3],而之移动了一个,但是我们知道这样是没有意义的浪费效率的移动,于是做出改进,特别的当next[j]=k,pat[j]=pat[k]的时候,那么就不和pat[k]比较,而是和pat[next[k]]比较,即使再往前一个,做出nextval数组,有点像递归,再等于就再往前修正和pat[next[k]]比较,为了不混淆next数组,我们就用nextval数组
记住j、k里面,k一般比j小,我们主要为了找连续元素中的相邻项。
我们比较的时候流程如下,
j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
next | 0 | 1 | 2 | 3 | 4 |
next[j] | 0 | 1 | 2 | 3 | 4 |
k | 0 | 1 | 2 | 3 | 4 |
pat[j] | a | a | a | a | b |
pat[k] | a | a | a | a | b |
所以
//左斜上角相等,那么找头一个,头一个又会找头一个,实际上是你上步找过推导好的
//但是看起来像递归
//如果不等,那就对了,那这个就是了,直接移动到这里来,因为之前都是重复元素
//之前不行移动一个肯定也不行,直接快速移动
if(pat[j] == pat[next[j]]){
nextval[j] = nextval[next[j]];
} else {
nextval[j] = next[j];
}
总:
O(m+n)
代码如下(示例):
void get_nextval(SString pat, int & nextval[], int next[])
{
int i = 1, j = 0;
nextval[1] = 0;
while (i < pat[0]) {
if (j == 0 || pat[i] == pat[j]) {
++i;
++j;
if(pat[j] == pat[next[j]]){
nextval[j] = nextval[next[j]];
} else {
nextval[j] = next[j];
}
}
else
j = nextval[j];
}
}
对BF算法和KMP算法进行总结:
长度: txt:n pat:m
BF:
最好情况:不成功匹配都发生在pat的第一个字符 O(m+n)
最坏情况:不成功匹配都发生在pat的最后一个字符 O(m*n)
KMP:
求next数组:O(m)
总时间复杂度(包含了求next数组):O(m+n)