0、前言
串就是字符串,由数字、字母、下划线组成的一串字符。在C语言中式用双引号,像:“abcd” 就是一个字符串,存储方式类似字符数组,要注意的还是下标与位置相差1,即a[0]= ‘a’ ,a[2]=‘c’ ;还有就是 ‘\0’ 是结尾符。
串模式匹配有两个字符串,
S
S
S:主串;
T
T
T:子串,也称为模式串。
串匹配就是在主串中查找与模式串T相匹配的子串,若是匹配成功,则返回匹配子串出现的第一个位置。
BF(Brute-Force)算法是最简单直观的模式(串)匹配算法
KMP算法是在BF算法的改进,不用回溯,使用next函数,
1、BF算法
1.1、算法描述
BF算法就是你想的那样“暴力”算法,一个一个相匹配,直至成功或失败,若懂的话下面文字就不用看了,再往下有代码。
主串开始计数,遍历比较字符,主串字符与模式串字符是否相等,若相等,则接着比较(主串位置与模式串位置都往后移动一位比较);若不相等,则回溯,即主串计数返回到开始匹配的下一个位置。直至在主串中找到与模式串相同的串,返回值是匹配子串出现的第一个位置。若匹配不成功的结果是主串计数到末尾。
这就是BF算法是思想吧。下面是详细步骤,想看就看。
- (1)分别利用计数指针 i i i和 j j j指示主串 S S S和模式串 T T T中当前正待比较的字符位置, i i i的初始值为pos(指定主串中查找的起始位置), j j j初始值为1。
- (2) 如果两个串均未比较到串尾,即
i
i
i和
j
j
j小于等于
S
S
S和
T
T
T的长度是,则循环执行以下操作:
- S.ch[i] 和 T.ch[j]比较,若相等,则 i 和 j 均分别指示串中下一个位置,继续比较后继字符;
- 若不等,计数指针后退重新开始匹配,从主串的下一个字符 (i=i-j+2) 起再重新和模式串的第一个字符(j=1)比较
- (3)如果 j>T.length ,说明匹配成功,返回和模式 T T T中第一个字符相等的字符在主串 S S S的序号(i-T.length);否则匹配不成功,返回0.
1.2、BF代码
#include<iostream>
#include<string.h>
using namespace std;
#define maxlen 123 //串的最大长度
typedef struct{
char ch[maxlen+1]; //存储串的一维数组
int length; //串的长度
}sstring;
int Index_BF(sstring S,sstring T,int pos){
//BF算法匹配
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) return i-T.length;
else return 0;
}
int main()
{
sstring S,T;
int loc; //保存匹配位置
int pos=1; //查找起始位置,这里默认为1
cout<<"输入主串(第一个位置字符不计,一般为0(下同)):"<<endl;
cin>>S.ch;
S.length=strlen(S.ch)-1; //存储串长度
cout<<"输入模式串:"<<endl;
cin>>T.ch;
T.length=strlen(T.ch)-1;
loc=Index_BF(S,T,pos);
if(loc)
cout<<"匹配成功!"<<endl<<"匹配开始位置:"<<loc<<endl;
else
cout<<"主串中无此模式串";
return 0;
}
代码运行结果:
2、KMP算法
2.1、描述
KMP算法是在BF算法基础上的改进,由Knuth、Morris和Pratt同时设计实现。
其改进在于每当一趟匹配过程中比较不等时,不需要回溯
i
i
i指针,而是例用已经得到的”部分匹配“的结果向右”滑动“,接着继续进行比较。
要解决的问题就是当主串中第
i
i
i个字符与模式中第
j
j
j个字符“失配”时,主串中第
i
i
i个字符再与模式串中哪个字符再比较?
解决办法是用到next函数,其 next[j]=k ,k就是再进行比较的字符的位置。
模式串的next函数定义:
n
e
x
t
[
j
]
=
{
0
j
=
1
(
t
1
与
s
1
比
较
不
等
时
,
下
一
步
进
行
t
1
与
s
i
+
1
)
的
比
较
)
M
a
x
{
k
∣
1
<
k
<
j
且
有
“
t
1
t
2
⋯
t
k
−
1
”
=
“
t
j
−
k
+
1
t
j
−
k
+
2
⋯
t
j
−
1
”
}
1
k
=
1
(
不
存
在
相
同
子
串
,
下
一
步
进
行
t
1
与
s
i
的
比
较
next[j] = \begin{cases} 0 &j=1(t_1与s_1比较不等时,下一步进行t_1与s_{i+1})的比较) \\ Max &\lbrace k|1<k<j且有“t_1t_2\cdots t_{k-1}”=“t_{j-k+1}t_{j-k+2} \cdots t_{j-1} ”\rbrace\\ 1 &k=1(不存在相同子串,下一步进行t_1与s_i的比较\\ \end{cases}
next[j]=⎩⎪⎨⎪⎧0Max1j=1(t1与s1比较不等时,下一步进行t1与si+1)的比较){k∣1<k<j且有“t1t2⋯tk−1”=“tj−k+1tj−k+2⋯tj−1”}k=1(不存在相同子串,下一步进行t1与si的比较
手推计算
n
e
x
t
[
j
]
next[j]
next[j]值方法是:模式串第
j
j
j位置前缀(从第1位置,最大到 j-2 )与后缀(从第 j-1 往前到第2位置)最大相同数+1
2.2、KMP代码
#include<iostream>
#include<string.h>
using namespace std;
#define maxlen 123 //串的最大长度
typedef struct{
char ch[maxlen+1]; //存储串的一维数组
int length; //串的长度
}sstring;
void get_next(sstring T,int next[]){
//求模式串T的next函数值并存入到数组next中
int i=1,j=0;
next[0]=0; //下标0是闲置的
next[1]=0;
while(i<T.length){
if(j==0||T.ch[i]==T.ch[j]){
i++;j++;
next[i]=j;
}
else j=next[j];
}
}
int Index_KMP(sstring S,sstring T,int pos,int next[]){
int i,j;
i=pos;j=1;
while(i<=S.length && j<=T.length){
if(j==0 || S.ch[i]==T.ch[j]){
i++;j++;
}
else{
j=next[j];
}
}
if(j>T.length) return i-T.length;
else return 0;
}
int main()
{
sstring S,T;
int next[20],i;
int loc; //保存匹配位置
int pos=1; //查找起始位置,这里默认为1
cout<<"输入主串(第一个位置字符不计,一般为0(下同)):"<<endl;
cin>>S.ch;
S.length=strlen(S.ch)-1; //存储串长度
cout<<"输入模式串:"<<endl;
cin>>T.ch;
T.length=strlen(T.ch)-1;
get_next(T,next);
cout<<"next:"<<endl; //打印next值
for(i=1;i<T.length+1;i++)
cout<<next[i]<<" ";
cout<<endl;
loc=Index_KMP(S,T,pos,next);
if(loc)
cout<<"匹配成功!"<<endl<<"匹配开始位置:"<<loc<<endl;
else
cout<<"主串中无此模式串";
return 0;
}
代码运行结果:
2.3、next修正
前面定义的next函数在某些情况下有缺陷,就像模式串是“aaaabb”,主串“aaabaaaabba”时,当
i
=
4
i=4
i=4、
j
=
4
j=4
j=4时 S.[i]
≠
\neq
=T.[j] ,由 next[i]的指示还需进行
(
i
=
4
,
j
=
3
)
、
(
i
=
4
,
j
=
2
)
、
(
i
=
4
,
j
=
1
)
(i=4,j=3)、(i=4,j=2)、(i=4,j=1)
(i=4,j=3)、(i=4,j=2)、(i=4,j=1)直至
n
e
x
t
[
j
]
=
0
next[j]=0
next[j]=0,
i
i
i才往后移动一位。实际上,由于模式中1~3个字符和第4个字符都相等,因此不需要再和主串中第4个字符相比较,接着进行
i
=
5
,
j
=
1
i=5,j=1
i=5,j=1时的比较。计算next函数修正就是要干这件事的。
算法代码
void get_nextval(sstring T,int nextval[]){
int i=1,j=0;
nextval[0]=0;
nextval[1]=0;
while(i<T.length){
if(j==0||T.ch[i]==T.ch[j]){
i++;j++;
if(T.ch[i]==T.ch[j]) nextval[i]=nextval[j];
else nextval[i]=j;
}
else j=nextval[j];
}
}
总结
BF和KMP算是模式串匹配里最经典的算法了的。
BF算法算是比较蛮力的方法,一个一个遍历,假如主串长度为n,模式串长度为m;
最好情况下平均时间复杂度为O(n+m);
最坏情况下平均时间复杂度是O(n×m);
虽然BF算法的时间复杂度是O(n×m),但一般情况下,其实际的执行时间近似于O(n×m),因此还是很实用。KMP算法仅当模式串与主串之间存在许多“部分匹配”的情况下,才显得比BF快得多。
KMP算法最大特点是不需要回溯,仅对主串从头到尾扫描一遍(这对处理从外设输入的庞大文件很有效,可以边读边匹配,而无需回头重读)
看图:
上面这个图分四个运行结果,从运行时间上来看,KMP是比BF算法快点,当主串字符“部分重复”的不多时,这俩也没什么太大区别;对next的修正来说,只有遇到特殊情况的时候,就“重复”多的时候,作用就比较明显。