KMP算法
在一个字符串中查找另一个子字符串的位置是常用的字符串操作之一。
在通常情况下,我们的写法是这样的。
// Exam5-1.cpp
#include <iostream>
using namespace std;
int Search(char *s, char *t)
{
int k, i,j, m, n;
m=strlen(s);
n=strlen(t);
for(k=0;k<m-n+1;k++)
{
i=k;
j=0;
while(i<m&& j<n && s[i]==t[j])
{
i++;
j++;
}
if(j==n)
returnj;
}
return -1;
}
int main(int argc, char *argv[])
{
chars[100], t[100];
cin>>s>>t;
cout<<Search(s, t)<<endl;
returnEXIT_SUCCESS;
}
这个程序的算法是相当“朴素”的:变量k是在字符串s中搜索t的起点,用指针i与j分别在s与t中扫描,逐一比较s[i]与t[j]是否相同。若遇到不相同的情况,则将起点k在s中后移一个字符,继续搜索。客观地讲,在两个字符串都不很长的情况下,这个算法的执行效率还是可以的。
但在某些特殊情况下,这个算法的效率问题就会显现出来,它最大的问题就在于:当出现s[i]!=t[j]时,指针i要回到前面的字符重复比较。
比如这种情况:
s: ababcabcacbab
t: abcac
当起点k=2时,如表-1所示
|
|
| k,i |
|
|
|
|
|
|
|
|
|
|
s | a | b | a | b | c | a | b | c | a | c | b | a | b |
t |
|
| a | b | c | a | c |
|
|
|
|
|
|
|
|
| j |
|
|
|
|
|
|
|
|
|
|
表-1
i从k(即2)开始,j从0开始比较,前四对字符都是比较成功的(表-1中的灰色部分),只在最后一个字符的比较时出现了问题,见表-2:
|
|
| k |
|
|
| i |
|
|
|
|
|
|
s | a | b | a | b | c | a | b | c | a | c | b | a | b |
t |
|
| a | b | c | a | c |
|
|
|
|
|
|
|
|
|
|
|
|
| j |
|
|
|
|
|
|
表-2
此时s[i]= =’b’,而t[j]= =’c’。比较失败后,内层循环结束,k移动到3,i也将回复到3,j回复到0,重新开始比较——尽管i最多时已经到6,它还是要回到3,这明显是有重复计算出现。
KMP算法对前述算法的改进之处就是变量i不需回复。
还从表-2分析,此时出现了s[i]!=t[j]的情况,我们的问题是:若变量i原地不动,它应该和t的哪一个字符继续进行比较呢(即变量j的值应修改为多少)?
在挑选j值时需要保证:新的t[j]之前的部分应该是与字符串s匹配成功的,通过观察,发现当j=1时,可以满足要求。见表-3:
|
|
|
|
|
|
| i |
|
|
|
|
|
|
S | a | b | a | b | c | A | b | c | a | c | b | a | b |
T |
|
|
|
|
| A | b | c | a | c |
|
|
|
|
|
|
|
|
|
| j |
|
|
|
|
|
|
表-3
此时,t[j]前面的部分与s[i]前面的等长部分是完全相同的,因此可以“放心大胆”地从此时的s[i]与t[j]开始,继续比较下去。
下面分析当s[i]!=t[j]时,求下一个j的算法。
分析表-2与表-3的t一行,发现表-3中j之前的部分,恰好是表-2中j之前的部分的后缀,而t这一行标明的都是字符串t的内容,表-3不过是将t的位置后移了,因此我们也可以说:表-3中j之前的部分,也是表-2中j之前的部分的前缀。
所以,若有s[i]!=t[j],找下一个j的方法是:在字符串t里j之前的部分中,找到即是前缀又是后缀的那个字符串(且要最长的那个)——假定它的长度为k,那么下一个j就取k(因为C++数组下标从0开始)。同时也得到另外一条结论:若有s[i]!=t[j],下一个j的选择只依赖于字符串t本身的性质,与字符串s无关,且这个值是应该固定的。
定义整数数组n[],n[j]存储当s[i]!=t[j]时,j的下一取值。
我们用递推的方法确定数组n[]。
首先在n[0]处设置监视哨,规定n[0]= -1(即字符串最小可用下标0的前一个数字,在后面的讨论中将看到这一设置的好处:它可以将原本的三种情况,归结为两种情况)。
然后用递推方法确定其它值,假定n[0]~n[j]的值都已经确定了,现由已知的这些数据来确定n[j+1]的值。
看另一个t的例子:见表-4
|
|
|
|
| J | j+1 |
|
|
|
|
|
|
|
n | -1 | 0 | 0 | 0 | 1 |
|
|
|
|
|
|
|
|
t | a | b | c | a | B | D | d |
|
|
|
|
|
|
t |
|
|
| a | B | C | a | b | d | d |
|
|
|
|
|
|
|
| K |
|
|
|
|
|
|
|
|
表-4
t=”abcabdd”,且我们已经确定n[0]~n[4]的值,现确定n[5]。当j=4时,记k=n[4]=1,由前面的讨论知道,在j之前有长度为k(即1)的子串即是前缀也是后缀(灰部分)。在表-4所示的情况下,又有t[j]==t[k],因此我们可以肯定:在j+1之前的部分中,最长的那个“即是前缀又是后缀的子串的长度一定是2,即k+1,因此有n[j+1]=k+1,即n[5]=2。
这只是一种情况,下面讨论另一种t[j]!=t[k]的情况,看表-5。
|
|
|
|
|
| j |
|
|
|
|
|
|
|
n | -1 | 0 | 0 | 0 | 1 | 2 |
|
|
|
|
|
|
|
t | a | b | c | a | b | d | d |
|
|
|
|
|
|
t |
|
|
| a | b | c | a | b | d | d |
|
|
|
|
|
|
|
|
| k |
|
|
|
|
|
|
|
表-5
此时已经确定n[0]~n[5]的值,现确定n[6]。已经有t[j]!=t[k],j+1之前的即是前缀又是后缀的最长子串已经不能由简单的由“延长”来得到了,但我们知道另一个有用的信息,即:若在t[k]处出现字符不相同,我们应该将k的值变为n[k],由于k<j,因此这个n[k]是已经确定的。通过这种方法不停缩小k值,直到出现两种情况:(1)若t[k]= =t[j],可以参考前一种方案;(2)若k= = -1,说明找不到合适的子串,要从t[0]开始比较,此时就可以用到监视哨了——n[j+1]=k+1。
综上,求数组n[]的函数Next()可以这样写出:
char t[100], s[100];
int n[100], m;
void Next()
{
int i, k;
k=-1;
n[0]=k;
i=0;
while(i<m)
if(k<0|| t[k]==t[i])
{
k++;
n[i+1]=k;
i++;
}
else
k=n[k];
}
当有了数组n[]之后,在s中搜索t时,若有s[i]!=t[j],则取j=n[j]继续比较就是了,若出现j<0或s[i]= =t[j]的情况,i、j都后移。代码如下:
【例5】KMP算法的实现。
从键盘读入两个字符串,在第一个字符串中查找第二个字符串的位置,利用KMP算法。
代码如下:
#include <iostream>
using namespace std;
char t[100], s[100];
int n[100], m;
void Next()
{
int i, k;
k=-1;
n[0]=k;
i=0;
while(i<m)
if(k<0|| t[k]==t[i])
{
k++;
n[i+1]=k;
i++;
}
else
k=n[k];
}
int Match()
{
int i=0,j=0;
while(i<int(strlen(s)) && j<int(strlen(t)))//必须做类型转换
if(j<0|| s[i]==t[j])
{
i++;
j++;
}
else
j=n[j];
if(j==strlen(t))
returni-j;
else
return-1;
}
int main(int argc, char *argv[])
{
cin>>s>>t;
m=strlen(t);
Next();
cout<<Match()<<endl;
returnEXIT_SUCCESS;
}
这个代码中,尤其要注意类型转换的那个地方,前面已经说过,strlen的返回值是无符号整数,而j可能是负数,若写j<strlen(t)的话,系统会将它自动转换为无符号整数——大的很呢,出现逻辑错误。
programKMP;
vars,t:string;
next:array[1..100]of longint;
i:longint;
procedurecount_next;
var i,j:integer;
begin
i:=1;j:=0;next[1]:=0;
while i<length(t) do begin
if (j=0)or(t[i]=t[j]) thenbegin
inc(i);
inc(j);
if (t[i]=t[j]) thennext[i]:=next[j] else next[i]:=j;
end
else j:=next[j];
end;
end;
procedureKMP_index;
var i,j:integer;
changed:boolean;
begin
count_next;
i:=1;j:=1;
changed:=false;
while (i<=length(s))and(j<=length(t))do begin
if (j=0)or(s[i]=t[j]) then begin
inc(i);
inc(j);
end
else j:=next[j];
if j>length(t) then begin
writeln('Place:',i-length(t));
changed:=true;j:=1;
end;
end;
if not changed then writeln('No Answer');
end;
begin
readln(s);
readln(t);
KMP_index;
end.
三、KMP算法
在一个字符串中查找另一个子字符串的位置是常用的字符串操作之一。
在通常情况下,我们的写法是这样的。
// Exam5-1.cpp
#include <iostream>
using namespace std;
int Search(char *s, char *t)
{
int k, i,j, m, n;
m=strlen(s);
n=strlen(t);
for(k=0;k<m-n+1;k++)
{
i=k;
j=0;
while(i<m&& j<n && s[i]==t[j])
{
i++;
j++;
}
if(j==n)
returnj;
}
return -1;
}
int main(int argc, char *argv[])
{
chars[100], t[100];
cin>>s>>t;
cout<<Search(s, t)<<endl;
returnEXIT_SUCCESS;
}
这个程序的算法是相当“朴素”的:变量k是在字符串s中搜索t的起点,用指针i与j分别在s与t中扫描,逐一比较s[i]与t[j]是否相同。若遇到不相同的情况,则将起点k在s中后移一个字符,继续搜索。客观地讲,在两个字符串都不很长的情况下,这个算法的执行效率还是可以的。
但在某些特殊情况下,这个算法的效率问题就会显现出来,它最大的问题就在于:当出现s[i]!=t[j]时,指针i要回到前面的字符重复比较。
比如这种情况:
s: ababcabcacbab
t: abcac
当起点k=2时,如表-1所示
|
|
| k,i |
|
|
|
|
|
|
|
|
|
|
s | a | b | a | b | c | a | b | c | a | c | b | a | b |
t |
|
| a | b | c | a | c |
|
|
|
|
|
|
|
|
| j |
|
|
|
|
|
|
|
|
|
|
表-1
i从k(即2)开始,j从0开始比较,前四对字符都是比较成功的(表-1中的灰色部分),只在最后一个字符的比较时出现了问题,见表-2:
|
|
| k |
|
|
| i |
|
|
|
|
|
|
s | a | b | a | b | c | a | b | c | a | c | b | a | b |
t |
|
| a | b | c | a | c |
|
|
|
|
|
|
|
|
|
|
|
|
| j |
|
|
|
|
|
|
表-2
此时s[i]= =’b’,而t[j]= =’c’。比较失败后,内层循环结束,k移动到3,i也将回复到3,j回复到0,重新开始比较——尽管i最多时已经到6,它还是要回到3,这明显是有重复计算出现。
KMP算法对前述算法的改进之处就是变量i不需回复。
还从表-2分析,此时出现了s[i]!=t[j]的情况,我们的问题是:若变量i原地不动,它应该和t的哪一个字符继续进行比较呢(即变量j的值应修改为多少)?
在挑选j值时需要保证:新的t[j]之前的部分应该是与字符串s匹配成功的,通过观察,发现当j=1时,可以满足要求。见表-3:
|
|
|
|
|
|
| i |
|
|
|
|
|
|
S | a | b | a | b | c | A | b | c | a | c | b | a | b |
T |
|
|
|
|
| A | b | c | a | c |
|
|
|
|
|
|
|
|
|
| j |
|
|
|
|
|
|
表-3
此时,t[j]前面的部分与s[i]前面的等长部分是完全相同的,因此可以“放心大胆”地从此时的s[i]与t[j]开始,继续比较下去。
下面分析当s[i]!=t[j]时,求下一个j的算法。
分析表-2与表-3的t一行,发现表-3中j之前的部分,恰好是表-2中j之前的部分的后缀,而t这一行标明的都是字符串t的内容,表-3不过是将t的位置后移了,因此我们也可以说:表-3中j之前的部分,也是表-2中j之前的部分的前缀。
所以,若有s[i]!=t[j],找下一个j的方法是:在字符串t里j之前的部分中,找到即是前缀又是后缀的那个字符串(且要最长的那个)——假定它的长度为k,那么下一个j就取k(因为C++数组下标从0开始)。同时也得到另外一条结论:若有s[i]!=t[j],下一个j的选择只依赖于字符串t本身的性质,与字符串s无关,且这个值是应该固定的。
定义整数数组n[],n[j]存储当s[i]!=t[j]时,j的下一取值。
我们用递推的方法确定数组n[]。
首先在n[0]处设置监视哨,规定n[0]= -1(即字符串最小可用下标0的前一个数字,在后面的讨论中将看到这一设置的好处:它可以将原本的三种情况,归结为两种情况)。
然后用递推方法确定其它值,假定n[0]~n[j]的值都已经确定了,现由已知的这些数据来确定n[j+1]的值。
看另一个t的例子:见表-4
|
|
|
|
| J | j+1 |
|
|
|
|
|
|
|
n | -1 | 0 | 0 | 0 | 1 |
|
|
|
|
|
|
|
|
t | a | b | c | a | B | D | d |
|
|
|
|
|
|
t |
|
|
| a | B | C | a | b | d | d |
|
|
|
|
|
|
|
| K |
|
|
|
|
|
|
|
|
表-4
t=”abcabdd”,且我们已经确定n[0]~n[4]的值,现确定n[5]。当j=4时,记k=n[4]=1,由前面的讨论知道,在j之前有长度为k(即1)的子串即是前缀也是后缀(灰部分)。在表-4所示的情况下,又有t[j]==t[k],因此我们可以肯定:在j+1之前的部分中,最长的那个“即是前缀又是后缀的子串的长度一定是2,即k+1,因此有n[j+1]=k+1,即n[5]=2。
这只是一种情况,下面讨论另一种t[j]!=t[k]的情况,看表-5。
|
|
|
|
|
| j |
|
|
|
|
|
|
|
n | -1 | 0 | 0 | 0 | 1 | 2 |
|
|
|
|
|
|
|
t | a | b | c | a | b | d | d |
|
|
|
|
|
|
t |
|
|
| a | b | c | a | b | d | d |
|
|
|
|
|
|
|
|
| k |
|
|
|
|
|
|
|
表-5
此时已经确定n[0]~n[5]的值,现确定n[6]。已经有t[j]!=t[k],j+1之前的即是前缀又是后缀的最长子串已经不能由简单的由“延长”来得到了,但我们知道另一个有用的信息,即:若在t[k]处出现字符不相同,我们应该将k的值变为n[k],由于k<j,因此这个n[k]是已经确定的。通过这种方法不停缩小k值,直到出现两种情况:(1)若t[k]= =t[j],可以参考前一种方案;(2)若k= = -1,说明找不到合适的子串,要从t[0]开始比较,此时就可以用到监视哨了——n[j+1]=k+1。
综上,求数组n[]的函数Next()可以这样写出:
char t[100], s[100];
int n[100], m;
void Next()
{
int i, k;
k=-1;
n[0]=k;
i=0;
while(i<m)
if(k<0|| t[k]==t[i])
{
k++;
n[i+1]=k;
i++;
}
else
k=n[k];
}
当有了数组n[]之后,在s中搜索t时,若有s[i]!=t[j],则取j=n[j]继续比较就是了,若出现j<0或s[i]= =t[j]的情况,i、j都后移。代码如下:
【例5】KMP算法的实现。
从键盘读入两个字符串,在第一个字符串中查找第二个字符串的位置,利用KMP算法。
代码如下:
#include <iostream>
using namespace std;
char t[100], s[100];
int n[100], m;
void Next()
{
int i, k;
k=-1;
n[0]=k;
i=0;
while(i<m)
if(k<0|| t[k]==t[i])
{
k++;
n[i+1]=k;
i++;
}
else
k=n[k];
}
int Match()
{
int i=0,j=0;
while(i<int(strlen(s)) && j<int(strlen(t)))//必须做类型转换
if(j<0|| s[i]==t[j])
{
i++;
j++;
}
else
j=n[j];
if(j==strlen(t))
returni-j;
else
return-1;
}
int main(int argc, char *argv[])
{
cin>>s>>t;
m=strlen(t);
Next();
cout<<Match()<<endl;
returnEXIT_SUCCESS;
}
这个代码中,尤其要注意类型转换的那个地方,前面已经说过,strlen的返回值是无符号整数,而j可能是负数,若写j<strlen(t)的话,系统会将它自动转换为无符号整数——大的很呢,出现逻辑错误。
programKMP;
vars,t:string;
next:array[1..100]of longint;
i:longint;
procedurecount_next;
var i,j:integer;
begin
i:=1;j:=0;next[1]:=0;
while i<length(t) do begin
if (j=0)or(t[i]=t[j]) thenbegin
inc(i);
inc(j);
if (t[i]=t[j]) thennext[i]:=next[j] else next[i]:=j;
end
else j:=next[j];
end;
end;
procedureKMP_index;
var i,j:integer;
changed:boolean;
begin
count_next;
i:=1;j:=1;
changed:=false;
while (i<=length(s))and(j<=length(t))do begin
if (j=0)or(s[i]=t[j]) then begin
inc(i);
inc(j);
end
else j:=next[j];
if j>length(t) then begin
writeln('Place:',i-length(t));
changed:=true;j:=1;
end;
end;
if not changed then writeln('No Answer');
end;
begin
readln(s);
readln(t);
KMP_index;
end.
个人看法: 其实第一次看这个还是感觉挺难的。不过确实比之前那种贼暴力的方式简单了不少。效率肯定是上去了。但还是有些不太明白……说不定多练练就好了呢!
下周要学Dijkstra、Bellman_Ford、SPFA、Floyd的算法复杂度的比较……很难的样子……我在考虑我还去不去……