BF算法以及KMP算法

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、BF是什么?
  • 二、循序渐进,由BF到KMP算法
    • 1.BF
    • 2.从BF到KMP
    • 3.next和nextval
  • 总结


前言

提示:BF、KMP算法

主要是用来记录一个很重要的算法kmp,以及是怎么推过来的


一、BF是什么?

BF算法实际上就是一种暴力算法,我们比较字符串,分为模式串和文本串,分别用 pat 表示模式串以及 txt 表示文本串,不管是BF算法还是KMP算法,我们都是在 txt 中查找子串 pat 的下标,如果存在,返回这个字串的起始索引,否则则返回-1。

二、循序渐进,由BF到KMP算法

1.BF

作为暴力算法来说,比较的思路非常简单,其实就是分别确立一个下标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;
	}
} 

2.从BF到KMP

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 = 1int 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];
		}
	} 
}

3.next和nextval

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)

你可能感兴趣的:(C++,DP,ACM,算法)