飞道的博客

理解数据结构教材中--KMP算法

394人阅读  评论(0)

1.一篇博文

KMP算法是一种字符串匹配算法,第一次接触KMP算法是在数据结构的教材中。为了弄懂这个算法,我在网上查阅了很多资料,很多博文都说其内容很好理解,但本人看完总是似懂非懂。
这个算法在掌握后会觉得非常简单,可其描述总是有些令人难以理解,这里推荐一篇2013年的文章,相信你看完后一定能理解KMP算法
KMP算法博文
不过有一个问题是,此理解方法似与《数据结构与算法(第四版 廖明宏)》中的P63页对于KMP算法的解释有所出入。

2.书中解释

对于书中的解释,其重点为一个公式:
N e x t [ j ] = { 0 , j = 1 M a x { k ∣ 1 < k < j } , S u b s t r ( T , 1 , k − 1 ) = S u b s t r ( T , j − k + 1 , k − 1 ) 1 , 其他情况 Next[j]=

{ 0 , j = 1 M a x { k | 1 < k < j } , S u b s t r ( T , 1 , k 1 ) = S u b s t r ( T , j k + 1 , k 1 ) 1 , 其他情况
Next[j]=0,Max{ k1<k<j},1,j=1Substr(T,1,k1)=Substr(T,jk+1,k1)其他情况
注:

  • 书中的字符串第一个字符的索引位是1。
  • 函数 S u b s t r ( T , n , m ) Substr(T,n,m) Substr(T,n,m)的含义是:对于字符串T,从第n位开始,截取m个字符。也就是截取一段字符串。
  • 举例: t [ 20 ] = " 8 h e l l o t o m " t[20]="8hellotom" t[20]="8hellotom" S u b s t r ( t , 2 , 4 ) = " e l l o " Substr(t,2,4)="ello" Substr(t,2,4)="ello". t [ 0 ] t[0] t[0]存放字符串长度,因此第一个字符的索引位是1。

模式串 t t t 的每一个 t j t_j tj 都对应一个 k k k 值,此 k k k 值仅依赖于模式串 t t t 本身字符序列的构成,而与主串 s s s 无关。用 n e x t [ j ] next[j] next[j] 表示 t j t_j tj 对应的 k k k 值。

3.对比书与博文

我们先以简单的语言来概括书与博文的要点。

  • 书中,重点为公式:
    S u b s t r ( T , 1 , k − 1 ) = S u b s t r ( T , j − k + 1 , k − 1 ) Substr(T,1,k-1)=Substr(T,j-k+1,k-1) Substr(T,1,k1)=Substr(T,jk+1,k1)
  • 博文中,我们不妨用 t a b l e [ j ] table[j] table[j]来表示部分匹配表,这个表中的部分匹配值就是前缀后缀最长共有元素的长度。

这里我们用一个例子来说明:

模式串 t = " a b a a b c a c " t="abaabcac" t="abaabcac" 或可写成 t = " 8 a b a a b c a c " t="8abaabcac" t="8abaabcac",求其 n e x t next next t a b l e table table

书中:

j 1 2 3 4 5 6 7 8
n e x t [ j ] ( 也 即 k ) next[j](也即k) next[j](k) 0 1 1 2 2 3 1 2

博文:

j 1 2 3 4 5 6 7 8
t a b l e [ j ] table[j] table[j] 0 0 1 1 2 0 1 0

初看,我们会感觉 n e x t next next t a b l e table table毫无关联,于是我们就会思考为什么是这个结果。
其实很好理解,在博文中提到了所谓的部分匹配表的用法:

模式串移动位数 = 已匹配的字符数 - 最后一个正确匹配位对应的 t a b l e table table

而这个 t a b l e table table表的含义是:前缀后缀最长共有元素的长度。

再看书中公式: S u b s t r ( T , 1 , k − 1 ) = S u b s t r ( T , j − k + 1 , k − 1 ) Substr(T,1,k-1)=Substr(T,j-k+1,k-1) Substr(T,1,k1)=Substr(T,jk+1,k1)
相信大家都感觉到了,这个公式中的 k-1也隐含表达:前缀后缀共有元素的长度,再加上一个 M a x { k ∣ 1 < k < j } Max\{k|1<k<j\} Max{ k1<k<j},即表示最长共有元素的长度。

  • 其中前缀为: t 1 t_1 t1~ t k − 1 t_{k-1} tk1后缀为: t j − k + 1 t_{j-k+1} tjk+1 ~ t j − 1 t_{j-1} tj1
  • 并且其最长共有元素的长度为: k − 1 k-1 k1,而这同时也是 t a b l e table table表的含义。即 k − 1 = t a b l e [ j ] k-1=table[j] k1=table[j],即 n e x t [ j ] − 1 = t a b l e [ j ] next[j]-1=table[j] next[j]1=table[j]

然而我们对上述两表根据 n e x t [ j ] − 1 = t a b l e [ j ] next[j]-1=table[j] next[j]1=table[j] 关系对照发现,笑死,根本对不上。

其实只是因为我们忽略了一个小问题:
对于公式:前缀为: t 1 t_1 t1~ t k − 1 t_{k-1} tk1后缀为: t j − k + 1 t_{j-k+1} tjk+1 ~ t j − 1 t_{j-1} tj1
我们发现后缀的结尾是 t j − 1 t_{j-1} tj1 而不是 t j t_j tj ,这就是问题所在。相信看到这里童鞋们一定恍然大悟了,没错,我们得错开一位
对于 n e x t next next表,它的第 j j j 位的公式对应的后缀是 t j − 1 t_{j-1} tj1 ,也就是对应 t a b l e table table表的第 j − 1 j-1 j1 位。即有对应关系: n e x t [ j ] − 1 = t a b l e [ j − 1 ] next[j]-1=table[j-1] next[j]1=table[j1]

如此看来:
t a b l e [ 1 ] = 0 = n e x t [ 2 ] − 1 = 1 − 1 table[1]=0=next[2]-1=1-1 table[1]=0=next[2]1=11
t a b l e [ 2 ] = 0 = n e x t [ 3 ] − 1 = 1 − 1 table[2]=0=next[3]-1=1-1 table[2]=0=next[3]1=11
t a b l e [ 3 ] = 1 = n e x t [ 4 ] − 1 = 2 − 1 table[3]=1=next[4]-1=2-1 table[3]=1=next[4]1=21
t a b l e [ 4 ] = 1 = n e x t [ 5 ] − 1 = 2 − 1 table[4]=1=next[5]-1=2-1 table[4]=1=next[5]1=21
t a b l e [ 5 ] = 2 = n e x t [ 6 ] − 1 = 3 − 1 table[5]=2=next[6]-1=3-1 table[5]=2=next[6]1=31
t a b l e [ 6 ] = 0 = n e x t [ 7 ] − 1 = 1 − 1 table[6]=0=next[7]-1=1-1 table[6]=0=next[7]1=11
t a b l e [ 7 ] = 1 = n e x t [ 8 ] − 1 = 2 − 1 table[7]=1=next[8]-1=2-1 table[7]=1=next[8]1=21
table[8]=0=next[9]-1=???-1
好的,现在都对应上了。。。除了 n e x t [ 9 ] next[9] next[9]不存在,也就是 t a b l e [ 8 ] table[8] table[8]对应不上,这又是为什么呢?

我们要理解 t a b l e table table表的用法,当 t t t 有一位和主串 s s s 不同,而之前都相同,此时就要用到 t a b l e table table表。我们不妨设不同的那位是 t k t_k tk 。现在计算模式串需要移动的位数:

模式串需要移动的位数 = 已匹配位数 - 最后一个正确匹配位对应的 t a b l e table table

即: 移 动 位 数 = ( k − 1 ) − t a b l e [ k − 1 ] 移动位数 = (k-1)-table[k-1] =(k1)table[k1]

好,现在回归之前的问题: t a b l e [ 8 ] table[8] table[8]对应不上。
我们看看什么情况下才会用到 t a b l e [ 8 ] table[8] table[8] k = 9 k=9 k=9时对吧,可是模式串一共只有8位,第9位根本不用考虑!

明白了吧,其实 t a b l e [ 8 ] table[8] table[8] 根本就用不到,我们不求它都可以。

唉,好像又出现了个问题,就是当模式串 t t t 的首位就与主串 s s s 不同,这时我们用公式:

模式串需要移动的位数 = 已匹配位数 - 最后一个正确匹配位对应的 t a b l e table table

已匹配位数为0,最后一个正确匹配位对应的 t a b l e table table 值没有定义。这就是问题所在。
解决方法很简单:

  1. 我们给 t a b l e [ 0 ] table[0] table[0] 定义为 -1。并且其仍满足 n e x t next next t a b l e table table 的对应关系: t a b l e [ 0 ] = − 1 = n e x t [ 1 ] − 1 = 0 − 1 table[0]=-1=next[1]-1=0-1 table[0]=1=next[1]1=01
    这样需要移动的位数 = 0 - (-1) = 1位 。。。 over
  2. 在代码中遇到这种情况,仅需用if判断出来,然后手动令其移动1位即可。。。over

好了,至此就没啥问题了。总结来看,KMP算法是利用了模式串本身存在的重复性进行优化的,这种重复性即前缀后缀那里。似乎很好理解。

4. 代码

既然博文与教材并不冲突,那么下面给出利用 t a b l e table table 表实现KMP算法的c语言代码。其优点就在于 t a b l e table table 表的计算很好理解,即前缀后缀最长共有元素的长度。

  • 代码中的索引用 t 0 t_0 t0代表第一个字符
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void my_gettable(char *t,int table[]);
int my_kmp(char *s,char *t,int table[]);

int main(){
   
	char s[20]="abaabcaabaabcaca",t[20]="abaabcac";
	int table[20];
	int i,m;
	my_gettable(t,table);
	for(i=0;i<strlen(t);i++)
		printf("table[%d]=%d\n",i,table[i]);
	m=my_kmp(s,t,table);
	if(m==-1)
		printf("没找到!");
	else
		printf("首次出现索引:%d",m);
	
	return 0;
}

void my_gettable(char *t,int table[]){
   
	int i,j=0,k,m;
	for(i=0;i<strlen(t);i++){
   
		table[i]=i;
		k=j+1;
		while(k<=i){
   
			for(m=k;m<=i;m++){
   
				if(t[j]!=t[m]){
   
					table[i]--;
					break;	
				}
				else
					j++;
			}
			j=0;
			k++;
			if(m==i+1)
				break;
		}
	}
}

int my_kmp(char *s,char *t,int table[]){
   
	int i=0,j=0;//i为s的指针,j为t的指针 
	while(i<strlen(s) && j<strlen(t)){
   
		if(s[i]==t[j]){
   
			i++;
			j++;
		}
		else if(j==0)//这就是上面提到的用if判断出来,手动加1的地方
			i++;
		else 
			j=table[j-1]; //这里很好理解,不必考虑什么递归思想什么的。
			//先按照博文思路,计算出模式串需要移动的位数,已匹配位数:j ,所以移动位数m=j-table[j-1].
			//接下来可以自己画图理解:把串后移m位,相当于把串t的指针前移m位。即j=j-m=j-(j-table[j-1])=table[j-1]
	}
	if(j<strlen(t))
		return -1;
	else
		return i-strlen(t);
}



转载:https://blog.csdn.net/qq_43475997/article/details/113823216
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场