天天看点

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)

BF算法

算法思想:

比较简单,定义i,和j分别指向主串和字串的头部),依次向后比较,若比较失败,主串和字串都需要回溯(i=比较轮数+1的位置,j回到0位置)

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)

元素相同,故i,j均向后移一位

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)

元素不同,i退回第二个位置。j变成0

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)

元素还是不同,i退到第三个位置

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)
数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)
数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)

结束

#include<stdio.h>
#include<string.h>
bool BF(char a[],char b[]){
	int index1 = 0;//指向a的头部 
	int index2 = 0;//指向b的头部 
	for(int i = 0;i<strlen(a);i++){//轮数 
		if(a[index1]==b[index2]){
			index1++;
			index2++;
		}else{
			index1 = i+1;
			index2 = 0;
		}
		if(index2==strlen(b)) return true; 
	}
	  return false;
}
int main(){
	char a[6] = {'a','c','a','b','d','a'};
	char b[3] = {'a','b','d'};
	char c[3] = {'a','b','e'};
	if(BF(a,b))printf("匹配\n");
	else printf("不匹配\n");
	if(BF(a,c))printf("匹配\n");
	else printf("不匹配\n");
	return 0;
} 
           

KMP算法

BF算法的弊端:

每次j下标都要回到0号下标,当主串和字串匹配失败时,主串进行回溯会影响效率,回溯之后,主串与字串有些部分比较是没有必要的

以下图为例,当i=j=5的时候匹配失败

按照BF算法,i=1,j=0,但这显然是多余的,因为子串第一个字符是A,令主串指针回溯到B的位置没有必要

(主串前六个字符我们都是访问过的),只需要令i=3,j=0就可以继续比较了。

更进一步,子串第二个字符是B,而i=4刚好指向B,那么可以令i = 4,j = 1

同理可以令i = 5,j = 2

我们发现i和最初没有变化,只有j的位置变化了

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)

整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)

事先我们会计算每个字串【0,k】的前缀和后缀公共部分的最大长度,信息保存在next数组中。一旦匹配失败,就调取这个子串【0,i-1】的next数组信息(假设i下标时匹配失败),j移动

因为这个重叠部分之前已经比较过了,不用再次比较

算法实现

next数组

next数组是字符串每一位及之前的字符串中,前缀和后缀公共部分的最大长度的集合

next[i]储存的是string中前i+1位字符串前缀和后缀的最长长度

比如ptr字符串(abxabwabxad),那么next数组就有11个元素

next[0]=-1(无前后缀)

next[1]=0(ab无公共前后缀)

next[2]=0(abx中无公共前后缀)

next[3]=1(abxa公共前后缀最长为a,长度为1)

next[4]=2(abxab公共前后缀最长为ab,长度为2)

next[5]=0(abxabw中无公共前后缀)

next[6]=1(abxabwa公共前后缀最长为a,长度为1)

next[7]=2(abxabwab公共前后缀最长为ab,长度为2)

next[8]=3(abxabwabx公共前后缀最长为abx,长度为3)

next[9]=4(abxabwabxa公共前后缀最长为abxa,长度为4)

next[10]=0(abxabwabxad中无公共前后缀)

注:前缀是第一个元素到倒数第二个元素

后缀是第二个元素到倒数第一个元素

next数组求法,是自身与自身的匹配

求next【k】,k=0,1,2,。。。,N

取第一个串的后半部分(int)(k/2)个数

和第二个串前一部分

如求下面一个串的next【6】:这里先不讨论next数组求法,最后再分析

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)
#include<stdio.h>
#include<string.h>
void getNext(char a[],int *next){
	next[0]=-1;
	int k = -1;
  for (int q = 1; q < strlen(a); q++)
    {
        while (k > -1 && a[k + 1] != a[q])
        {
            k = next[k];//往前回溯
        }
        if (a[k + 1] == a[q])//如果相同,k++
        {
            k = k + 1;
        }
        next[q] = k;
    }
}
bool KMP(char a[],char b[]){
    int next[strlen(a)];
	getNext(a,next);
	int i = 0;
	int j = 0;
	int len1 = strlen(a);
	int len2 = strlen(b);
	while(i<len1&&j<len2){
		if(a[i]==b[j]||j==-1){ //注意一定要j = -1,前后公共部分长度为0,就没有必要回溯了
			i++;
			j++;
		}else{
			j = next[j];
		}
	}
//	printf("%d,%d ",i,j);
	if(j==strlen(b)) return true;
	return false;
}
int main(){
	char a[] = "abacababa"; 
	int next[strlen(a)];
	getNext(a,next);
	//for(int i = 0;i< strlen(a);i++)printf("%d ",next[i]);
	char b[] = "cba";
	char c[] = "ca";
	char d[] = "baba";
	if(KMP(a,b))printf("匹配\n");
	else printf("不匹配\n");
	if(KMP(a,c))printf("匹配\n");
	else printf("不匹配\n");
	if(KMP(a,d))printf("匹配\n");
	else printf("不匹配\n");

	return 0;
}
           

这里很难理解的点是

while (k > -1 && str[k + 1] != str[q])
        {
            k = next[k];
        }
           

从代码上看:我们往前找到一个k,使a[k + 1]==a[q]

为什么要找这个:因为要找前后缀的新的公共部分,a[k+1]是前缀的公共部分+1,a[q]是原来后缀的最后一个+1(即新增的元素)

注意;这里的公共部分是上一次求出的,也就是最后一个是a(用递归的思想去理解,我们不关注前面具体怎么来的)

为什么要回溯:因为匹配不对了–》因此现有的前缀和后缀公共长度是0?

这显然是不对的,你不能保证这个串前缀完全没有能和后缀匹配的部分。

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)

比如这个求next【7】,最后一个数不同,是不是意味着公共长度为0?显然不一定(我怎么知道前面有没有C出现?)

这个时候我们需要回溯到前面找到C

回溯不一定是k–(这样太麻烦,而且据说会出错)

看黄色的部分前缀后缀重叠部分是ABA,长度为3,那么next【k】=2,回溯一次(用于比较的是a【k+1】)

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)

还不同,还需要回溯,现在黄色部分是aba,前后缀重叠部分为a,那么next【k】=0,回溯一次(用于比较的是a【k+1】)

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)

还不同,继续回溯重叠部分为0,那么next【k】=-1;

举一个更明显的例子

数据结构与算法笔记----BF算法与KMP算法(图解+C语言实现+代码分析)

虽然不匹配了,但是明显还有公共的部分【a,g】

结合整体代码总结一下

for (int q = 1; q < strlen(a); q++)
    {
        while (k > -1 && a[k + 1] != a[q])
        {
            k = next[k];//往前回溯
        }
        if (a[k + 1] == a[q])//如果相同,k++
        {
            k = k + 1;
        }
        next[q] = k;
    }
           

循环求next数组,q是最后一位,而k是前缀的公共部分的最后一个(上一次求出的)的下一个,如果这个数和a【q】相等,那么公共部分长度可以加1(因为原来前缀后缀公共部分长度是K,现在后缀多了一个元素,去判断公共部分以外的第一个元素是否与这个元素重合),否则我们进行回溯,直到找到一个新的(更小的)公共部分

继续阅读