搞定单模式匹配(简单,KMP)
模式匹配是查找的一种,分为单模式匹配和多模式匹配。查找,就是在一个集合中查找一个或多个元素,查找一个元素就叫单模式匹配,查找多个元素就是多模式匹配,这里只探讨单模式匹配。虽然模式匹配看上去与数字的查找不一样,但是本质上任然是一种查找,比如在“aabaabaabaac”中查找“aabaac”,对计算机来说,处理的仍然是在集合{aabaab, abaaba, baabaa, aabaab, abaaba, baabaa, aabaac}中查找“aabaaac”,这是计算机的流处理特性决定的。所谓的简单模式匹配算法,就是顺序查找,代码如下:
#include<stdio.h>
#include<string.h>
int searchstr(char S[],char T[])
{
int i=1,j=1;
while(i<=strlen(S) && j<=strlen(T))// i 或 j 越界跳出
{
if(S[i-1]==T[j-1]) {++i,++j;}//同则后移
else {i=i-j+2;j=1;}//异则结束本次比较,准备下一次比较
}
if(j>strlen(T)) return i-j+1;//判断跳出类型, i 还是 j 越界跳出
else return 0;
}
main()
{
char S[]="aabaabaabaac";
char T[]="aabaac";
int temp=searchstr(S,T);
if(temp) printf("the position is : %d\n",temp);
else printf("ERROR!\n");
}
KMP算法并没有盲目搜索,而是做了一些逻辑推理跳过了一些不必要的搜索,下面就演示一下KMP是如何推理的,我们以在“aabaabaabaac”中查找“aabaac”为例,假设对比到第六位发现不匹配,这时前面5位都是匹配的。
那么我们结束这次对比,进行下一次对比,如下图:
“aabaa?”匹配失败,我们跳过了“abaaa??”与“baa???”这两次对比直接来到了“aa????”,为什么会这样,简单推理一下就知道了,模版是“aabaac”,很显然“abaaa??”与“baa???”都不可能匹配,只有“aa????”才【有可能】与模版匹配。事实上这个可能匹配的字符串是有一定特征的,匹配失败时我们得到aabaa这个相同的前缀,aabaa有若干前缀和若干后缀(规定缀不能为字符串本身,aabaa不是aabaa的缀),前后缀相同的有a和aa,我们称之为同缀,我们只找最长同缀aa,它的长度对我们很重要。接下来回到“aa????”这次对比:
“aa????”和模版“aabaac”对比,最长同缀aa根本不用比,直接从红色的部分开始对比,因为i本来就是指在这里的,所以i看上去是不动的,只用重定位j就行了,这个j的新位置就是书上说的next数组,next[j-1]表示第j位对比失败后,j跳转的位置。在本例中第6位对比失败,j跳转到3,即next[5]中存储的是3。3是如何推理出来的?其实就是【1+最长同缀的长度】。现在假设是“aab?”第四位匹配失败,那么相同前缀是aab,aab没有最长同缀,所以它的最长同缀长度是0,按照规律j重定位到1+0=1位,即新一次对比的第一位,这就是为什么要加1的原因。当第一位匹配失败时,next数组不起任何作用,但我们还是象征性的把next[0]设为0,它表示第一位对比失败,当检测到j为0,我们需要把i和j都后移一位,这样j就刚好到1了,于是任何模版的next[0]都为0。任意字符X的最长同缀是0,于是任何模版的next[1]都为1。
为什么KMP这么不好理解,原因在于书本上没有说清楚两次逻辑推理过程,第一次推理是直接跳过若干次比较来到新一次的比较的第一位,第二次推理是在新一次的对比中,又跳过了前面若干位的比较,最终定位到正确的位置。这个过程中i并没有变,而j按照某种规律重新定位了,这个规律就是【j=1+最长同缀的长度】。毫无疑问,next数组是根据模版推理出来的,所以需要事先进行计算,我们假设next已经求解,KMP代码如下(其代码与简单匹配的代码高度类似):
int KMP(char S[],char T[],int next[],int pos)
{
int i=pos,j=1;//添加功能,从 S 中的pos位置开始查找
while(i<=strlen(S) && j<=strlen(T))// i 或 j 越界跳出
{
if(j==0 || S[i-1]==T[j-1]) {++i,++j;}//第一位对比失败和任意位对比成功都处理成i,j同时后移
else {j=next[j-1];}//按照某种规律重定位 j
}
if(j>strlen(T)) return i-j+1;//判断跳出类型, i 还是 j 越界跳出
else return 0;
}
KMP函数写出来之后,不要以为就万事大吉了,离实现KMP算法还远着呢,整个KMP算法的精髓就在于推理next数组,如何推理next数组?这个时候我们就要有分治思想了。
看看我们以上在做什么?我们在枚举7长度字符串可能的next值,它的取值可能是next[6]+1到0。但是它看上去就像是在“aba?”中查找“abaa”,注意此时“abaa”的next数组已经求解出来了,当第四位匹配出错,我们跳过了:
直接来到了:
其实我们还可以再跳过几个字符:
这个过程是不是似曾相识呢?没错,这就是KMP的重定位j,因为abaa的next已经求解,所以我们确实是可以使用next数组的。我们注意到在匹配成功之前,i始终是不动的,i的含义其实就是“正在求解i长度字符串的next[i]”。而匹配成功时j的含义就是“i长度字符串的最长同缀”。此时j+1就是next[i]值,然后我们把i,j同时后移处理i+1长度字符串最长同缀。j=0,没有啥用,它仅仅做为一个标志,标志找不到最长同缀,即最长同缀为0。根据这些我们就可以完成getnext代码,代码如下:
void getnext(char T[],int next[])
{
next[0]=0;
next[1]=1;//没必要求解,恒成立
int i=2,j=1;//直接从求解2长度字符串开始
while(i<=strlen(T))
{
if(j==0||T[i-1]==T[j-1])//字符相等标志找到最长同缀,j 为 0 标志最长同缀为 0
{
next[i]=j+1;// i 的含义是 i 长度字符串,j的含义是最长同缀为 j
++i;++j;//i,j整体后移,表示已经求解i长度字符串,下一次将处理i+1长度字符串
}
else j=next[j-1];//j重定位
}
}
充分理解了这段代码之后,我们还可以把它做的更简洁:
void getnext(char T[],int next[])
{
next[0]=0;
next[1]=1;
int i=2,j=1;
while(i<=strlen(T))
{
if(j==0||T[i-1]==T[j-1]) next[i++]=++j;//使用跟新前的 i 值和跟新后的 j 值
else j=next[j-1];
}
}
全部KMP代码如下:
#include<stdio.h>
#include<string.h>
#define MAXSIZE 100
int KMP(char S[],char T[],int next[],int pos)
{
int i=pos,j=1;//添加功能,从 S 中的pos位置开始查找
while(i<=strlen(S) && j<=strlen(T))// i 或 j 越界跳出
{
if(j==0 || S[i-1]==T[j-1]) {++i,++j;}//第一位对比失败和任意位对比成功都处理成i,j同时后移
else {j=next[j-1];}//按照某种规律重定位 j
}
if(j>strlen(T)) return i-j+1;//判断跳出类型, i 还是 j 越界跳出
else return 0;
}
void getnext(char T[],int next[])
{
next[0]=0;
next[1]=1;
int i=2,j=1;
while(i<=strlen(T))
{
if(j==0||T[i-1]==T[j-1]) next[i++]=++j;//使用跟新前的 i 值和跟新后的 j 值
else j=next[j-1];
}
}
main()
{
char S[]="vfyabaababm";
char T[]="abaababm";
//计算 next 数组
int next[MAXSIZE];
getnext(T,next);
//显示 next 数组
printf("the next array is : ");
for(int i=0;i<strlen(T);i++)
printf("%d ",next[i]);
printf("\n");
//显示查找结果
int temp=KMP(S,T,next,1);
if(temp) printf("the position is : %d\n",temp);
else printf("ERROR!\n");
}