数据结构 : 串的模式匹配算法

一、串的存储结构

1、串的顺序存储
# define MAXSIZE 800
typedef struct{
char ch[MAXSIZE];
int length;
}SString;
2、串的链式存储
# define CHUNKSIZE 8
typedef struct Chunk{
char ch[CHUNKSIZE];
struck Chunk *next;
}Chunk;
typedef struct{
Chunk *head,*tail;
int length;
}LString;

二、串的模式匹配算法

给定字符串t和s,假设t为子串,s为主串,利用“模式匹配算法”找出s中与t匹配的子串,返回子串位置。

1、BP算法
  • BP算法是一种最简单的模式匹配算法,其具体思路如下:
    令i=j=0;
    step1:比较t[i]和s[j],如果相等,则++i, ++j;当i=length of t 时,表示s中存在子串与t相匹配,返回子串位置;
    step2:如果t[i] != s[j],则:++j;i=0;比较t[i]和s[j]的大小;
    step3:重复执行step1和step2。
  • BP算法的缺点
    每当t[i] != s[j]时,都要倒回 i=0, j=j-i,重新比较t[i]和s[j];造成了计算浪费;
// BP算法
//利用顺序存储来实行BP算法
int index(SString s,SString t,int pos){  //从s的pos位置开始,与t进行比较,查看s中是否有与t相匹配的子串,如有,返回子串的位置
int i=0;
int j=pos;
while(i<=t.length && j<= s.length){
if(t.ch[i] == s.ch[j]){++i;++j}
else{i=1;j=j-i+2;} //字符串下标从1开始
}
if(i>t.length){return j-t.length;}
else{return 0;}
}
//设主串的长度为n,子串的长度为m,则:
//最好的情况下:BP时间复杂度为 O(m+n);
//最坏的情况下:BP时间复杂度为O(m*n);
2、KMP算法

KMP算法针对BP算法中 经常需要回溯的问题,做出了优化,其具体思路是:
假设主串为 <s1s2…sn>,模式串为<t1t2…tm>,当主串第j个字符与子串第i个字符不匹配时,假设:主串第j个字符(j指针不回溯),应该与子串的第k个字符相匹配,即有等式:sj-1,sj-1,sj-2,…,sj-k+1 = tk-1,tk-1,tk-2,…,t1;(1)
而主串与子串 原来 已经匹配的部分有 等式:ti-k+1,ti-k+2,…,ti-1 = sj-k+1,sj-k+2,…,sj-1;(2)
通过等式(1)和等式(2)可知:t1,t2,…,tk-1 = ti-k+1,ti-k+2,…,ti-1;(3)
我们从上面几个公式反推可以得到这样的结论:
当主串第j个字符和子串第i个字符不等时,我们可以将子串向右移到第k个字符,使得主串第j个字符 和 子串第 k个字符进行匹配,此时,子串中第t1,t2,…,tk-1 必定与 主串中第sj-l+1,sj-k,…,sj-1相匹配。由此,匹配仅需从子串第k个字符与主串第j个字符开始向后进行比较即可,无需在回溯。
若令next[i] = k,则next[i]代表当 模式子串第i个字符 和 主串第j个字符 失配时,模式子串退回到 第 next[i]=k 个字符 与主串第j个字符进行匹配,如若仍然不匹配,则模式子串 退回到第 next[k] 个字符,与主串进行匹配,直到 模式子串 与 主串 相配,或者 模式子串 位置为0,则i++,j++,进行下一对字符的匹配;

模式子串中的next函数设定如下:
next[i] =
condition1:0 , i=1,下一步进行ti+1,sj+1的比较
condition2: Max{k | 1<k<i,且有 <t1,t2,…,tk-1> = <tj-k+1,tj-k+2,…,tj-1>}
condition3: 1,k=1(不存在相同的子串,下一步进行t1与sj的比较)

根据next函数,我们可以将“BP算法”修改如下:

int index(SString s,SString t,int pos){
i=1; //字符串从下标1开始标记
j=pos;
while(i<= t.length && j<= s.length){
if(t.ch[i] == s.ch[j] || i==0){++i; ++j}
else{i = next[i];}
}
if(i>t.length){return j-t.length;}
else{return 0;}

那么上述算法中的next的函数如何计算呢?接下来介绍一下next函数的推导过程:
首先,由定义可知,next[1] = 0;
Assume:模式子串同时为主串 和 子串,假定现有next[i] = k,则有:<ti-k+1,ti-k,…,ti-1> = <t1,t2,…,tk-1>。
condition1:如果 主串第 i个字符 与 子串 第k个字符相同,则有:next[i+1] = k+1 = next[i] + 1;
condition2:如果 主串第i个字符 与 子串 第k个字符不相同,则子串回溯,next[k]=k’,此时,如果主串第i个字符与子串第k’个字符相同,则有,next[i+1] = k’+ 1=next[k] + 1,…。
从上述的两种condition我们可以得出一个结论,如果主串第i个字符与子串第k个字符相同,则next[i+1] = k+1 = next[i] + 1;如果主串第i个字符与子串第k个字符不相同,则一直回溯next[k],直到next[k]=0,同时++i,++k。又或者,回溯,存在next[k]=k’,使得主串第i个字符与子串第k’个字符相同,则next[i+1] = next[k] + 1。(note that:next[i] = k,指的是,当主串第j个字符与子串第i个字符不匹配时,回溯到子串第k个字符继续进行比较。子串的第i个字符与子串的第k个字符不相同,但是,他们之前的k-1个字符相同)。
根据上述next函数的推导过程,给出next函数计算代码:

void get_next(SString t,int next[]){
next[1]=0;//根据定义可知
j=1; //代表主串下标
i=0; //代表子串下标
while(j<t.length){
if(t.ch[i] == t.ch[j] || i==0){++i;++j;next[j]=i;} //要么主串第j个字符和子串第i个字符相同,则next[j+1]=i+1
else{i=next[i]} //要么主串第j个字符和子串第i个字符不相同,则回溯子串比较下标
}
}

虽然BP算法的时间复杂度为O(m*n),但是,一般情况下,其时间复杂度接近于O(m+n),因此目前依然被很多人使用。KMP算法主要适用于模式和主串中存在很多 部分匹配 的情况。
KMP的一大优点是 当出现不匹配时,主串不需要在回溯,向前走一步 或 保持不动 就好,这对处理从外设输入的庞大文件很有效,可以边读入边匹配,而无需回头重读。

虽然前面所讲的KMP算法已经在BP算法的基础上 improve 很多,但是,KMP中next函数的计算还是有很多可以改进的地方的,下面就看一下 改进的next函数思路:
给定模式子串为:aaaab,主串为:aaabaaaab,进行模式匹配:
当匹配到第4位时,s[4] != t[4],此时,如果回溯模式子串的话next[4]=3,但是,此时t[4]=t[3],因此,此时与s[4]的匹配结果仍然不变,同样的next[3]=2,next[2]=1,这两次与s[4]的匹配结果也仍然不变,也就是说,模式子串从t[4]进行了3次 本不需要的 回溯,其实 模式子串 和 主串 完全可以从 t[1] ,s[5]开始匹配,为了解决 原始next函数的这种低效性,提出了 改进的next函数计算法:
即对于next[i] = k,如果ti = tk,则主串sj和子串ti比较不等时,则不需要在进行sj和tk的比较,直接到 next[k]=k’, 且 ti != tk’ 进行比较即可。根据 这种思想,我们修改next函数如下:

void get_nextval(SString t,int next[]){
j=1;  //代表主串下标
i=0;  //代表子串下标
nextval[1] = 0; //根据定义得来
while(j<t.length){
if(t.ch[i] == t.ch[j] || i==0){
++i;
++j;
if(t.ch[i] != t.ch[j]){nextval[j] = i;}
else{nextval[j] = nextval[i];}
}
else{i = nextval[i];}
}
}

参考资料:《数据结构》 严蔚敏

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sarah ฅʕ•̫͡•ʔฅ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值