KMP匹配的模式算法——保姆级解读(图文版)

同样道理,在我们知道T串中首字符“a”与T中后面的字符均不相等的前提下,T串的“a”与S串后面的“c”、"d”、“e”也都可以在①之后就可以确定是不相等的,所以这个算法当中②③④⑤没有必要,只保留①⑥即可,如图所示。

之所以保留⑥中的判断是因为在①中 T[6]≠S[6],尽管我们已经知道T[1]≠T[6],但也不能断定T[1]一定不等于S[6],因此需要保留⑥这一步。

有人就会问,如果T串后面也含有首字符“a”的字符怎么办呢?

我们来看下面一个例子,假设S=“abcabcabc”,T=“abcabx”。对于开始的判断,前5个字符完全相等,第6个字符不等,如图的①。此时,根据刚才的经验,T的首字符“a”与T的第二位字符“b"、第三位字符“℃”均不等,所以不需要做判断,图的朴素算法步骤②③都是多余。

因为T的首位“a”与T第四位的“a”相等,第二位的“b”与第五位的“b”相等。而在①时,第四位的“a”与第五位的“b”已经与主串S中的相应位置比较过了,是相等的,因此可以断定,T的首字符“a”、第二位的字符“b”与S的第四位字。

符和第五位字符也不需要比较了,肯定也是相等的——之前比较过了,还判断什么,所以④⑤这两个比较得出字符相等的步骤也可以省略。

也就是说,对于在子串中有与首字符相等的字符,也是可以省略一部分不必要的判断步骤。如图所示,省略掉右图的T串前两位“a”与“b”同S串中的4、5位置字符匹配操作。

对比这两个例子,我们会发现在①时,我们的主值,也就是主串当前位置的下标是6,23④5,i值是2、3、4、5,到了⑥,i值才又回到了6。即我们在朴素的模式匹配算法中,主串的i 值是不断地回溯来完成的。而我们的分析发现,这种回溯其实是可以不需要的——正所谓好马不吃回头草,我们的 KMP模式匹配算法就是为了让这没必要的回溯不发生。

既然主值不回溯,也就是不可以变小,那么要考虑的变化就是j值了。通过观察也可发现,我们屡屡提到了T串的首字符与自身后面字符的比较,发现如果有相等字符,j值的变化就会不相同。也就是说,这个j值的变化与主串其实没什么关系,关键就取决于T串的结构中是否有重复的问题。

比如图中,由于T=“abcdex”,当中没有任何重复的字符,所以j就由6变成了1。而图中,由于T=“abcabx”,前缀的“ab”与最后“x”之前串的后缀“ab”是相等的。因此j就由6变成了3。因此,我们可以得出规律,j值的多少取决于当前字符之前的串的前后缀的相似度。

2,NEXT数组推导


具体如何推导出一个串的next数组值呢,我们来看一些例子。

1.T="abcdex”(如表所示)

1)当=1时,next[1]=0;

2)当j=2时,j由1到j-1就只有字符“a”,属于其他情况next[2]=1;

3)当j=3时,j由1到j-1串是“ab”,显然“a”与“b”不相等,属其他情况,next[3]=1;

4)以后同理,所以最终此T串的nexti]为011111。

2. T="abcabx”(如表所示)

1)当j=1时,next[1]=0;

2)当j=2时,同上例说明,next[2]=1;3)当j=3时,同上,next[3]=1;

4)当j=4时,同上,next[4]=1;

5)当j=5时,此时j由1到j-1的串是“abca”,前缀字符“a”与后缀字符

“a”相等(前缀用下划线表示,后缀用斜体表示),因此可推算出k值为2(由‘pr…pk-1’-‘P)j-ki1…p-1’,得到p1=p4)因此next[5]=2;

6)当j=6时,j由1到j-1的串是“abcab”,由于前缀字符“ab”与后缀“ab”

相等,所以next[6]=3。

可以根据经验得到如果前后缀一个字符相等,k值是2,两个字符k值是3,

3.T=““ababaaaba”(如表所示)

1)当=1时,next[1]=0;

2)当j=2时,同上next[2]=1;

3)当j=3时,同上next[3]=1;

4)当j=4时,j由1到j-1的串是“aba”,前缀字符“a”与后缀字符“a”相

等,next[4]=2;

5)当j=5时,j由1到j-1的串是“abab”,由于前缀字符“ab”与后缀“ab”

相等,所以next[5]=3;

6)当j=6时,j由1到j-1的串是“ababa”,由于前缀字符“aba”与后缀

"aba”相等,所以next[6]=4;

7)当j=7时,j由1到j-1的串是“ababaa”,由于前缀字符“ab”与后缀

“aa”并不相等,只有“a”相等,所以next[7]=2;

8)当j=8时,j由1到j-1的串是“ababaaa”,只有“a”相等,所以

next[8]=2;

9)当j=9时,j由1到j-1的串是“ababaaab”,由于前缀字符“ab”与后缀

“ab”相等,所以next[9]=3。

4, T="aaaaaaaab”(如表所示)

1)当j=1时,next[1]=0;

2)当j=2时,同上next[2]=1;

3)当j=3时,j由1到j-1的串是“aa”,前缀字符“a”与后缀字符“a”相

等,next[3]=2;

4)当j=4时,j由1到j-1的串是“aaa”,由于前缀字符“aa”与后缀“aa”相

等,所以next[4]=3;

  1. ………

6)当j=9时,j由1到j-1的串是“aaaaaaaa”,由于前缀字符“aaaaaaa”与后缀“aaaaaaa”相等,所以next[9]=8。

3,算法实现


/* 通过计算返回子串T的next数组。 */

void get_next(String T, int *next)

{

int i,k;

i=1;

k=0;

next[1]=0;

while (i<T[0]) /* 此处T[0]表示串T的长度 */

{

if(k0 || T[i] T[k])

{

++i;

++k;

next[i] = k;

}

else

k= next[k]; /* 若字符不相同,则k值回溯 */

}

}

/* 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0。 */

/* T非空,1≤pos≤StrLength(S)。 */

int Index_KMP(String S, String T, int pos)

{

int i = pos; /* i用于主串S中当前位置下标值,若pos不为1,则从pos位置开始匹配 */

int j = 1; /* j用于子串T中当前位置下标值 */

int next[255]; /* 定义一next数组 */

get_next(T, next); /* 对串T作分析,得到next数组 */

while (i <= S[0] && j <= T[0]) /* 若i小于S的长度并且j小于T的长度时,循环继续 */

{

if (j==0 || S[i] == T[j]) /* 两字母相等则继续,与朴素算法增加了j=0判断 */

{

++i;

++j;

}

else /* 指针后退重新开始匹配 */

j = next[j];/* j退回合适的位置,i值不变 */

}

if (j > T[0])

return i-T[0];

else

4,算法改进


对于get_next函数来说,若T的长度为m,因只涉及到简单的单循环,其时间复杂度为0(m),而由于主值的不回溯,使得 index_KMP 算法效率得到了提高,while循环的时间复杂度为O(n)。因此,整个算法的时间复杂度为O(n+m)。相较于朴素模式匹配算法的O((n-m+1)*m)来说,是要好一些。

这里也需要强调,KMP算法仅当模式与主串之间存在许多“部分匹配”的情况下才体现出它的优势,否则两者差异并不明显。

后来有人发现,KMP还是有缺陷的。比如,如果我们的主串 S="aaaabcde”,子串T=“aaaaax””,其next 数组值分别为012345,在开始时,当 i=5、j=5时,我们发现b”与“a”不相等,如图的①,因此j=next[5]=4,如图中的②,此时“b”与第4位置的“a”依然不等,j=next[4]=3,如图中的③,后依次是④⑥,直到j=next[1]=0时,根据算法,此时i++、j++,得到 i=6、j=1,如图中的⑥。

我们发现,当中的②③④⑤步骤,其实是多余的判断。由于T串的第二、三、四、五位置的字符都与首位的“a”相等,那么可以用首位 next[1]的值去取代与它相等的字符后续next[i]的值,这是个很好的办法。因此我们对求next函数进行了改良。

假设取代的数组为nextval,增加了加粗部分,代码如下:

/* 求模式串T的next函数修正值并存入数组nextval */

void get_nextval(String T, int *nextval)

{

int i,k;

i=1;

k=0;

nextval[1]=0;

while (i<T[0]) /* 此处T[0]表示串T的长度 */

{

if(k0 || T[i] T[k]) /* T[i]表示后缀的单个字符,T[k]表示前缀的单个字符 */

{

++i;

++k;

if (T[i]!=T[k]) /* 若当前字符与前缀字符不同 */

nextval[i] = k; /* 则当前的j为nextval在i位置的值 */

else

nextval[i] = nextval[k]; /* 如果与前缀字符相同,则将前缀字符的 */

/* nextval值赋值给nextval在i位置的值 */

}

else

k= nextval[k]; /* 若字符不相同,则k值回溯 */

}

}

四,全部代码

==========

#include “string.h”

#include “stdio.h”

#include “stdlib.h”

#include “math.h”

#include “time.h”

#define OK 1

#define ERROR 0

#define TRUE 1

#define FALSE 0

#define MAXSIZE 100 /* 存储空间初始分配量 */

typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */

typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */

typedef char String[MAXSIZE+1]; /* 0号单元存放串的长度 */

/* 生成一个其值等于chars的串T */

Status StrAssign(String T,char *chars)

{

int i;

if(strlen(chars)>MAXSIZE)

return ERROR;

else

{

T[0]=strlen(chars);

for(i=1;i<=T[0];i++)

T[i]=*(chars+i-1);

return OK;

}

}

Status ClearString(String S)

{

S[0]=0;/* 令串长为零 */

return OK;

}

/* 输出字符串T。 */

void StrPrint(String T)

{

int i;

for(i=1;i<=T[0];i++)

printf(“%c”,T[i]);

printf(“\n”);

}

/* 输出Next数组值。 */

void NextPrint(int next[],int length)

{

int i;

for(i=1;i<=length;i++)

printf(“%d”,next[i]);

printf(“\n”);

}

/* 返回串的元素个数 */

int StrLength(String S)

{

return S[0];

}

/* 朴素的模式匹配法 */

int Index(String S, String T, int pos)

{

int i = pos; /* i用于主串S中当前位置下标值,若pos不为1,则从pos位置开始匹配 */

int j = 1; /* j用于子串T中当前位置下标值 */

while (i <= S[0] && j <= T[0]) /* 若i小于S的长度并且j小于T的长度时,循环继续 */

{

if (S[i] == T[j]) /* 两字母相等则继续 */

{

++i;

++j;

}

else /* 指针后退重新开始匹配 */

{

i = i-j+2; /* i退回到上次匹配首位的下一位 */

j = 1; /* j退回到子串T的首位 */

}

}

if (j > T[0])

return i-T[0];

else

return 0;

}

/* 通过计算返回子串T的next数组。 */

void get_next(String T, int *next)

{

int i,k;

i=1;

k=0;

next[1]=0;

while (i<T[0]) /* 此处T[0]表示串T的长度 */

{

if(k0 || T[i] T[k])

{

++i;

++k;

next[i] = k;

}

else

k= next[k]; /* 若字符不相同,则k值回溯 */

}

}

/* 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0。 */

/* T非空,1≤pos≤StrLength(S)。 */

int Index_KMP(String S, String T, int pos)

{

int i = pos; /* i用于主串S中当前位置下标值,若pos不为1,则从pos位置开始匹配 */

int j = 1; /* j用于子串T中当前位置下标值 */

int next[255]; /* 定义一next数组 */

get_next(T, next); /* 对串T作分析,得到next数组 */

while (i <= S[0] && j <= T[0]) /* 若i小于S的长度并且j小于T的长度时,循环继续 */

{

if (j==0 || S[i] == T[j]) /* 两字母相等则继续,与朴素算法增加了j=0判断 */

{

++i;

++j;

}

else /* 指针后退重新开始匹配 */

j = next[j];/* j退回合适的位置,i值不变 */

}

if (j > T[0])

return i-T[0];

else

return 0;

}

/* 求模式串T的next函数修正值并存入数组nextval */

void get_nextval(String T, int *nextval)

{

int i,k;

i=1;

k=0;

nextval[1]=0;

while (i<T[0]) /* 此处T[0]表示串T的长度 */

{

if(k0 || T[i] T[k]) /* T[i]表示后缀的单个字符,T[k]表示前缀的单个字符 */

{

++i;

++k;

if (T[i]!=T[k]) /* 若当前字符与前缀字符不同 */

nextval[i] = k; /* 则当前的j为nextval在i位置的值 */

else

nextval[i] = nextval[k]; /* 如果与前缀字符相同,则将前缀字符的 */

/* nextval值赋值给nextval在i位置的值 */

}

else

k= nextval[k]; /* 若字符不相同,则k值回溯 */

}

}

int Index_KMP1(String S, String T, int pos)

{

int i = pos; /* i用于主串S中当前位置下标值,若pos不为1,则从pos位置开始匹配 */

int j = 1; /* j用于子串T中当前位置下标值 */

int next[255]; /* 定义一next数组 */

get_nextval(T, next); /* 对串T作分析,得到next数组 */

while (i <= S[0] && j <= T[0]) /* 若i小于S的长度并且j小于T的长度时,循环继续 */

{

if (j==0 || S[i] == T[j]) /* 两字母相等则继续,与朴素算法增加了j=0判断 */

{

++i;

总结

上述知识点,囊括了目前互联网企业的主流应用技术以及能让你成为“香饽饽”的高级架构知识,每个笔记里面几乎都带有实战内容。

很多人担心学了容易忘,这里教你一个方法,那就是重复学习。

打个比方,假如你正在学习 spring 注解,突然发现了一个注解@Aspect,不知道干什么用的,你可能会去查看源码或者通过博客学习,花了半小时终于弄懂了,下次又看到@Aspect 了,你有点郁闷了,上次好像在哪哪哪学习,你快速打开网页花了五分钟又学会了。

从半小时和五分钟的对比中可以发现多学一次就离真正掌握知识又近了一步。

人的本性就是容易遗忘,只有不断加深印象、重复学习才能真正掌握,所以很多书我都是推荐大家多看几遍。哪有那么多天才,他只是比你多看了几遍书。

tring T, int pos)

{

int i = pos; /* i用于主串S中当前位置下标值,若pos不为1,则从pos位置开始匹配 */

int j = 1; /* j用于子串T中当前位置下标值 */

int next[255]; /* 定义一next数组 */

get_nextval(T, next); /* 对串T作分析,得到next数组 */

while (i <= S[0] && j <= T[0]) /* 若i小于S的长度并且j小于T的长度时,循环继续 */

{

if (j==0 || S[i] == T[j]) /* 两字母相等则继续,与朴素算法增加了j=0判断 */

{

++i;

总结

上述知识点,囊括了目前互联网企业的主流应用技术以及能让你成为“香饽饽”的高级架构知识,每个笔记里面几乎都带有实战内容。

很多人担心学了容易忘,这里教你一个方法,那就是重复学习。

打个比方,假如你正在学习 spring 注解,突然发现了一个注解@Aspect,不知道干什么用的,你可能会去查看源码或者通过博客学习,花了半小时终于弄懂了,下次又看到@Aspect 了,你有点郁闷了,上次好像在哪哪哪学习,你快速打开网页花了五分钟又学会了。

从半小时和五分钟的对比中可以发现多学一次就离真正掌握知识又近了一步。

[外链图片转存中…(img-fRGKJR1x-1719279028524)]

人的本性就是容易遗忘,只有不断加深印象、重复学习才能真正掌握,所以很多书我都是推荐大家多看几遍。哪有那么多天才,他只是比你多看了几遍书。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值