串的模式匹配 解题心得体会
关于串,模式匹配是其一个很重要的问题。针对这个问题,书上讲了两种模式匹配的算法,即BF算法和KMP算法,下面针对这两种算法的实现谈谈我的心得。 |
一、BF算法的探索
【错误代码1】
#include
#include
using namespace std;
typedef struct{
char ch[1000002];
int length;
}SString;
void Index_BF(SString S, SString T, int pos){
int i,j;
i=pos;
j=1;
while ( i <= S.length && j <= T.length ) {
if ( S.ch[i]==T.ch[j] ) { ++i; ++j; }
else { i = i-j+2; j=1; }
if ( j>T.length )
{
cout<< i-T.length<>s>>t;
strcpy(S.ch,s);
strcpy(T.ch,t);
Index_BF(S, T , 1);
return 0;
}
分析:编译轻松通过,但是怎么都不能输入进去。参考其他同学的博客,发现我的这个问题有人也遇到过,原因在于在主函数中定义了两个长度为100W的数组,程序根本跑不动。
解决:参考博客发现有两种途径解决数组过大问题:①将数组定义为全局变量 ②动态分配数组
针对我这一题的具体情况,因为我是要用到strcpy函数来copy数组的内容的,所以第一种方式显然更适合。
【错误代码2】
#include
#include
using namespace std;
typedef struct{
char ch[1000002];
int length;
}SString;
char s[1000002]={0},t[1000002]={0};
SString S,T;
void Index_BF(SString S, SString T, int pos){
int i,j;
i=pos;
j=1;
cout<<"smooth"<T.length )
{
cout<< i-T.length<>s>>t;
cout<<"good"<
分析:这一次程序是顺利运行完了,就是没有想要的结果出来。
解决:为了查找程序在哪里断掉了,我在一些步骤之后设置了一些输出,比如cout<<"good"。此法说明我的问题出在BF算法上。经过仔仔细细的检查,发现BF算法中我打while循环的时候掉了一个括号,加上去就正常了。
接下来需要解决位置和下标的问题,我现在输出的还是下标,不是题目要求的位置。思考过后发现解决的途径有两种:改数组;改算法。这里我选择的是改算法的方法。
【正确程序1】
#include
using namespace std;
#include
//采用静态顺序存储结构(定长)
typedef struct{
char ch[1000001]; //存储串的一维数组
int length; //串的长度
}SString;
SString S,T;
char s[1000001];
char t[1000001];
//BF算法
//查找 模式T 在 主串S 中第pos个字符开始第一次出现的位置,并返回
//若不存在,则返回0 (T非空,1<=pos<=S.length)
int Index_BF(SString S,SString T,int pos)
{
int i,j;
i=pos;
j=0;
while(i<=S.length-1 && j<=T.length-1)
{
if(S.ch[i]==T.ch[j]){ //从各自的第一位开始比较,如果相同,比较下一位
++i;
++j;
}
else {//如果不同,主串指针回到 上次开始比较时的字符 的下一个字符,
//模式回到第一个字符,重新开始比较
i=i-j+1;
j=0;
}
}
if(j>T.length-1) //匹配成功
return i-T.length+1;//主串指针位置往回退模式长度个单位,就回到了该模式在主串中第一次出现的位置
else //匹配失败
return 0; //返回0(顺序存储的字符串是从下标为1的数组分量开始存储的,下标为0的分量闲置不用)
}
//主函数
int main()
{
cin>>s>>t;
strcpy(S.ch,s);
strcpy(T.ch,t);
S.length=strlen(S.ch);
T.length=strlen(T.ch);
cout<
现在把程序放到PTA上面跑,得到了15分,唯一错误的一个点就是超时。但这个是BF算法无法解决的了,所以我决定一鼓作气把他改成KMP算法。
一、KMP算法的探索
在真正着手写代码前,我花了很久阅读课本,也想了很久,终于弄明白了这个算法的核心思想。以下是我的思路:
①BF究竟是哪里麻烦了?
设想一种情况:主串是abcab... 子串是abcac... 如果按照BF算法我们知道,一旦b和c不匹配了,接下来比较的就是主串的第二个字符b和子串的第一个字符a,不匹配时主串和子串都右移。直到子串的首位字符a与主串的a对齐之前,我们做的都是无用功。这就是从一个具体的例子看出来的麻烦之处。
②如果说KMP可以跳过这些“无用功”,那什么情况下可以跳?这是一个特殊情况,真的能上升到一个算法的高度吗?
为了解决我以上的疑虑,我决定用抽象的模型进行说明。
设主串Si(大家不要把它看成硅元素的元素符号啦)与模式tj失配……这句话暗含(S1S2……Si-1与t1t2……tj-1是匹配的)
这里我们将主串Si固定死,让他与模式中的tk匹配(因为不知道,所以设k这个未知数,也就是说这时候模式要从j跳到k)即:
Si-k+1...Si-1 =t1...tk-1
我们由第一步的匹配就已经得到:
Si-k+1...Si-1 = tj-k+1...tj-1
把这两个式子联立,得到:
t1...tk-1 = tj-k+1...tj-1 |
③求k
以上我们得到了一个很重要的关系式,即t1...tk-1 = tj-k+1...tj-1,它隐含了我们想要知道的k,也就是子串该跳到哪一个地方这一信息。
观察式子,我们首先考虑一些边边角角的问题,比如k=1,j=1这些情况。
1)k=1 这时式子变成了t1...t0 = tj...tj-1 这种情况说明子串不符合要求,没有那个相等的部分。顺便也解决了我们第二问中的疑惑,其实KMP算法的优化是有条件的,要求子串与主串匹配的那部分有“相等的部分”。
这时候的处理办法也只有老老实实跳到t1与Si比较。模式啊模式!你若是堕落,KMP也救不了你!
2)j=1 其实就是模式的第一位就与主串Si不匹配。那还犹豫啥?直接把模式向右移一个呗。
④怎么用算法实现?
其实整个大框架在BF算法的基础上来说不用怎么改,只需要把回溯的那块儿稍微修改一下,即改成主串i不回溯,模式跳到k位置。
但是现在说来说去我们还是只有一串关系式,还有两个边界的情况,k到底怎么表示?对解决这个问题还是一脸懵逼。
阅读课本发现他使用一个next[j]函数来表示子串下一个回溯的位置。 其实想一想next[j]仅与模式有关,和主串半毛钱关系也没有。从已知的关系式t1...tk-1 = tj-k+1...tj-1入手,此时next[j] = k。我们求一求next[j+1]?这时候产生两种情况需要讨论:
1)若tk = tj 那么t1...tk = tj-k+1...tj 也就是next[j+1] = k+1 , 亦即next[j+1] = next[j] +1。
2)若tk!=tj 是不是有一种似曾相识的感觉?对了,这又是一次模式匹配,只不过此时的模式既充当了主串又充当模式。所以我们要把模式中的第next[j]个字符和主串中的第j个字符对齐进行比较。这种情况可以利用函数的递归调用,让j=next[j]。
【正确程序2】
#include
using namespace std;
#include
//采用静态顺序存储结构(定长)
typedef struct{
char ch[1000002]; //存储串的一维数组
int length; //串的长度
}SString;
SString S,T;
char s[1000002];
char t[1000002];
int nex[1000002];
//KMP算法
//查找 模式T 在 主串S 中第pos个字符开始第一次出现的位置,并返回
//若不存在,则返回0 (T非空,1<=pos<=S.length)
int Index_KMP(SString S,SString T,int next[])
{
int i,j;
i=j=0;
while(i<=S.length-1 && j<=T.length-1)
{
if(j==-1||S.ch[i]==T.ch[j]){ //从各自的第一位开始比较,如果相同,比较下一位
++i;
++j;
}
else {
j=next[j];
}
}
if(j>T.length-1) //匹配成功
return i-T.length+1;//
else //匹配失败
return 0;
}
void get_next(SString T,int next[]){
int i=0;
next[0]=-1;
int j=-1;
while(i>s>>t;
strcpy(S.ch,s);
strcpy(T.ch,t);
S.length=strlen(S.ch);
T.length=strlen(T.ch);
get_next(T,nex);
cout<
4.14补充:
①感谢640同学的提示,其实next数组这一段是我照书打的,只是根据需要改动了一点儿,但是其中有些关键问题还是没能理解透彻。比如说问我为什么next[i] = j就把我问到了。(后来弄明白了,把解释写进注释里)这也提醒我要争取把每一个细节都弄懂弄通,才能真正自己打代码。
②命名next问题,这个当时我也是一知半解,出错后改成nex通过后也没有去深究为什么。从这个问题也得到了几点收获:
1)遇到数组过大,可以考虑在静态存储区或堆内存中存储。2)命名有风险,代码需谨慎,不要撞上保留字啦。
---
下一周的计划之一是抽空做做前面几章的实践2,我感觉代码是越打越熟练的,所以要多加练习。而且上次小测成绩不是特别理想,一个很大的问题就是我有时候太依赖书本了,所以我的第二个目标是要培养自己不看书本打代码的能力,这也是对思维完备性的锻炼。