更新时间:2021年12月07日15时38分 来源:传智教育 浏览次数:
模式匹配的模型大概是这样的:给定两个字符串变量S和P,其中S成为目标串,其中包含n个字符,P称为模式串,包含m个字符,其中m<=n。从S的给定位置(通常是S的第一个位置)开始搜索模式P。如果找到,则返回模式P在目标串中的位置(即:P的第一个字符在S中的下标)。如果在目标串S中没有找到模式串P,则返回-1.这就是模式匹配的定义啦,下面来看看怎么实现模式匹配算法吧。
朴素的模式匹配算法非常简单,容易理解,大概思路是这样的:从S的第一个字符S0开始,将P中的字符依次和S中字符比较,若S0=P0 && …… && Sm-1 = Pm-1,则证明匹配成功,剩下的匹配无需进行了,返回下标0。若在某一步Si != Pi 则P中剩下的字符也不用比较了,不可能匹配成功了,然后从S中第二个字符开始与P中第一个字符进行比较,同理,也是知道Sm = Pm-1或者找到某个i使得Si != S-1为止。依次类推若知道以S中第n-m个开始字符为止,还没有匹配成功则证明S中不存模式P。(想想为什么这里强调是n-m)这个代码实现应该是非常简单的,具体开始参考strstr函数的内部实现。可以看看百度百科,给个链接http://baike.baidu.com/view/745156.htm,这里不写出来了,还得赶紧进入正题KMP呢。
朴素的模式匹配效率不高的主要原因是进行了重复的字符比较。下一次比较和上一次比较没有任何的联系,是朴素模式匹配的缺点,其实上一次比较的比较结果是可以利用的,这就产生了快速模式匹配。在朴素的模式匹配中,目标串S的下标移动是一步一步的,这其实并不好,移动步数没有必要为1。
现在不妨假设,当前匹配情况是这样的:S0 …… St St+1 …… St+j 与 P0 P1…… Pj ,现在正在尝试匹配的字符是St+j+1和Pj+1,并且St+j+1 != Pj+1,言外之意就是说St St+1……St+j和P0 P1……Pj是完全匹配的。那么这个时候,S中下一次匹配开始位置应该是什么呢??按照朴素的模式匹配,下次比较应该从St+1开始,并且令St+1和P0比较,但是在快速模式匹配中并不是这样,快速模式匹配选择St+j+1和Pk+1比较,K是什么呢?K是这样的一个值,使得P0 P1……Pk 和 Pj-k Pj-k+1……Pj完全匹配,不妨设k=next[j],因此P0 P1……Pk和St+j-k St+j-k+1 ……St+j完全匹配。那么下一次要进行匹配的两个字符应为St+j+1和Pk+1。S和P都没有回溯到下标0在进行比较,这就是KMP之所以快的原因啦。
现在关键问题来了,这个K怎么能得到呢?如果得到这个K值复杂度高,那这个思路就不好了,其实这个K呢,只和模式串P有关系,并且要求m个k,k = next[j],因此只要算一次存储到next数组中就可以了,并且时间复杂度和m有关系(线性关系)。看看具体怎么求next数组的值,即求k。
用归纳法求next[]:设next(0) = -1,若已知next(j) = k,欲求得next[j+1]。
(1)如果Pk+1 = Pj+1,显然next[j+1] = k+1.如果Pk+1 != Pj+1,则next[j+1] < next[j],于是寻找h < k 使得P0 P1……Ph = Pj-h Pj-h+1……Pj = Pk-h Pk-h+1……Pk。也就是说h = next(k);看出来了吧,这是个迭代的过程。(也就是以前的结果对求以后的值有用)
(2)如果不存这样的h,说明P0 P1……Pj+1中没有前后相等的子串,因此next[j+1] =-1.
(3)如果存在这样的h,继续检验Ph和Pj是否相等。知道找到这中相等的情况,或者确定为-1求next[j+1]的过程结束。
KMP算法它的实现过程接近人为进行模式匹配的过程。例如,对主串 A("ABCABCE")和模式串 B("ABCE")进行模式匹配,如果人为去判断,仅需匹配两次。
图 1 第一次人为模式匹配
第一次如图 1 所示,最终匹配失败。但在本次匹配过程中,我们可以获得一些信息,模式串中 "ABC" 都和主串对应的字符相同,但模式串中字符 'A' 与 'B' 和 'C' 不同。
因此进行下次模式匹配时,没有必要让串 B 中的 'A' 与主串中的字符 'B' 和 'C' 一一匹配(它们绝不可能相同),而是直接去匹配失败位置处的字符 'A' ,如图 2 所示:
图 2 第二次人为模式匹配
至此,匹配成功。若使用 BF 算法,则此模式匹配过程需要进行 4 次。
由此可以看出,每次匹配失败后模式串移动的距离不一定是 1,某些情况下一次可移动多个位置,这就是 KMP 模式匹配算法。
那么,如何判断匹配失败后模式串向后移动的距离呢?
模式串移动距离的判断
每次模式匹配失败后,计算模式串向后移动的距离是 KMP 算法中的核心部分。
其实,匹配失败后模式串移动的距离和主串没有关系,只与模式串本身有关系。
例如,我们将前面的模式串 B 改为 "ABCAE",则在第一次模式匹配失败,由于匹配失败位置模式串中字符 'E' 前面有两个字符 'A',因此,第二次模式匹配应改为如图 3 所示的位置:
图 3 模式匹配过程示意图
结合图 1、图 2 和图 3 不难看出,模式串移动的距离只和自身有关系,和主串无关。换句话说,不论主串如何变换,只要给定模式串,则匹配失败后移动的距离就已经确定了。
不仅如此,模式串中任何一个字符都可能导致匹配失败,因此串中每个字符都应该对应一个数字,用来表示匹配失败后模式串移动的距离。
注意,这里要转换一下思想,模式串向后移动等价于指针 j 前移,如图 4 中的 a) 和 b)。换句话说,模式串后移相当于对指针 j 重定位。
图 4 模式串后移等价于 j 前移
因此,我们可以给每个模式串配备一个数组(例如 next[]),用于存储模式串中每个字符对应指针 j 重定向的位置(也就是存储模式串的数组下标),比如 j=3,则该字符匹配失败后指针 j 指向模式串中第 3 个字符。
模式串中各字符对应 next 值的计算方式是,取该字符前面的字符串(不包含自己),其前缀字符串和后缀字符串相同字符的最大个数再 +1 就是该字符对应的 next 值。
前缀字符串指的是位于模式串起始位置的字符串,例如模式串 "ABCD",则 "A"、"AB"、"ABC" 以及 "ABCD" 都属于前缀字符串;后缀字符串指的是位于串结尾处的字符串,还拿模式串 "ABCD" 来说,"D"、"CD"、"BCD" 和 "ABCD" 为后缀字符串。
注意,模式串中第一个字符对应的值为 0,第二个字符对应 1 ,这是固定不变的。因此,图 3 的模式串 "ABCAE" 中,各字符对应的 next 值如图 5 所示:
图 5 模式串对应的 next 数组
从图 5 中的数据可以看出,当字符 'E' 匹配失败时,指针 j 指向模式串数组中第 2 个字符,即 'B',同之前讲解的图 3 不谋而合。
以上所讲 next 数组的实现方式是为了让大家对此数组的功能有一个初步的认识。接下来学习如何用编程的思想实现 next 数组。编程实现 next 数组要解决的主要问题依然是 "如何计算每个字符前面前缀字符串和后缀字符串相同的个数"。
仔细观察图 5,为什么字符 'C' 对应的 next 值为 1?因为字符串 "AB" 前缀字符串和后缀字符串相等个数为 0,0 + 1 = 1。那么,为什么字符 'E' 的 next 值为 2?因为紧挨着该字符之前的 'A' 与模式串开头字符 'A' 相等,1 + 1 = 2。
如果图 5 中模式串为 "ABCABE",则对应 next 数组应为 [0,1,1,1,2,3],为什么字符 'E' 的 next 值是 3 ?因为紧挨着该字符前面的 "AB" 与开头的 "AB" 相等,2 + 1 =3。
因此,我们可以设计这样一个算法,刚开始时令 j 指向模式串中第 1 个字符,i 指向第 2 个字符。接下来,对每个字符做如下操作:
如果 i 和 j 指向的字符相等,则 i 后面第一个字符的 next 值为 j+1,同时 i 和 j 做自加 1 操作,为求下一个字符的 next 值做准备,如图 6 所示:
图 6 i 和 j 指向字符相等
上图中可以看到,字符 'a' 的 next 值为 j +1 = 2,同时 i 和 j 都做了加 1 操作。当计算字符 'C' 的 next 值时,还是判断 i 和 j 指向的字符是否相等,显然相等,因此令该字符串的 next 值为 j + 1 = 3,同时 i 和 j 自加 1(此次 next 值的计算使用了上一次 j 的值)。如图 7 所示:
图 7 i 和 j 指向字符仍相等
如上图所示,计算字符 'd' 的 next 时,i 和 j 指向的字符不相等,这表明最长的前缀字符串 "aaa" 和后缀字符串 "aac" 不相等,接下来要判断次长的前缀字符串 "aa" 和后缀字符串 "ac" 是否相等,这一步的实现可以用 j = next[j] 来实现,如图 8 所示:
图 8 执行 j=next[j] 操作
从上图可以看到,i 和 j 指向的字符又不相同,因此继续做 j = next[j] 的操作,如图 9 所示:
图 9 继续执行 j=next[j] 的操作
可以看到,j = 0 表明字符 'd' 前的前缀字符串和后缀字符串相同个数为 0,因此如果字符 'd' 导致了模式匹配失败,则模式串移动的距离只能是 1。
这里给出使用上述思想实现 next 数组的 C 语言代码:
void Next(char*T,int *next){ next[1]=0; next[2]=1; int i=2; int j=1; while (i<strlen(T)) { if (j==0||T[i-1]==T[j-1]) { i++; j++; next[i]=j; }else{ j=next[j]; } } }
代码中 j=next[j] 的运用可以这样理解,每个字符对应的next值都可以表示该字符前 "同后缀字符串相同的前缀字符串最后一个字符所在的位置",因此在每次匹配失败后,都可以轻松找到次长前缀字符串的最后一个字符与该字符进行比较。
Next函数的缺陷
图 10 Next 函数的缺陷
例如,在图 10a) 中,当匹配失败时,Next 函数会由图 10b) 开始继续进行模式匹配,但是从图中可以看到,这样做是没有必要的,纯属浪费时间。
出现这种多余的操作,问题在当 T[i-1]==T[j-1] 成立时,没有继续对 i++ 和 j++ 后的 T[i-1] 和 T[j-1] 的值做判断。改进后的 Next 函数如下所示:
void Next(char*T,int *next){ next[1]=0; next[2]=1; int i=2; int j=1; while (i<strlen(T)) { if (j==0||T[i-1]==T[j-1]) { i++; j++; if (T[i-1]!=T[j-1]) { next[i]=j; } else{ next[i]=next[j]; } }else{ j=next[j]; } } }
使用精简过后的 next 数组在解决例如模式串为 "aaaaaaab" 这类的问题上,会大大提高效率,如图 11 所示,精简前为 next1,精简后为 next2:
图 11 改进后的 Next 函数
KMP 算法的实现
假设主串 A 为 "ababcabcacbab",模式串 B 为 "abcac",则 KMP 算法执行过程为:
第一次匹配如图 12 所示,匹配结果失败,指针 j 移动至 next[j] 的位置;
图 12 第一次匹配示意图
第二次匹配如图 13 所示,匹配结果失败,依旧执行 j=next[j] 操作:
图 13 第二次匹配示意图
第三次匹配成功,如图 14 所示:
图 14 第三次匹配示意图
很明显,使用 KMP 算法只需匹配 3 次,而同样的问题使用 BF 算法则需匹配 6 次才能完成。
KMP 算法的完整 C 语言实现代码为:
#include <stdio.h> #include <string.h> void Next(char*T,int *next){ int i=1; next[1]=0; int j=0; while (i<strlen(T)) { if (j==0||T[i-1]==T[j-1]) { i++; j++; next[i]=j; }else{ j=next[j]; } } } int KMP(char * S,char * T){ int next[10]; Next(T,next);//根据模式串T,初始化next数组 int i=1; int j=1; while (i<=strlen(S)&&j<=strlen(T)) { //j==0:代表模式串的第一个字符就和当前测试的字符不相等;S[i-1]==T[j-1],如果对应位置字符相等,两种情况下,指向当前测试的两个指针下标i和j都向后移 if (j==0 || S[i-1]==T[j-1]) { i++; j++; } else{ j=next[j];//如果测试的两个字符不相等,i不动,j变为当前测试字符串的next值 } } if (j>strlen(T)) {//如果条件为真,说明匹配成功 return i-(int)strlen(T); } return -1; } int main() { int i=KMP("ababcabcacbab","abcac"); printf("%d",i); return 0; }
运行结果为:
6