原创文章,转载请注明出处:http://blog.csdn.net/fastsort/article/details/9903153
1、字符串匹配
所谓字符串匹配,就是在主串S中寻找模式串P,如果存在,则返回P在S中的起始下标,否则返回不存在信息(这里用-1表示)。例如:
S=abcdabce,T=cda,
则返回2,如果T=cdd,则返回-1.
考虑C/C++中字符串下标都是从0开始,以下讨论都是在字符串下标从0开始的前提下进行【有特殊说明的除外】。
2、常规算法
在确定找到高效算法前,“暴力”算法(brute force)虽然效率差强人意,但是总是值得信任的。
字符串匹配算法也是这样。直接匹配也很简单,把主串S的第一个字符和模式串P的第一个字符对齐,然后两个串同时向后比较:
如果一直到P的结尾都匹配,很好,匹配成功,返回0;
如果中间存在不匹配的字符,那么把S的第二个字符和P的第一个字符对齐,然后两个串同时向后比较,直到继续S的第三个字符或者是匹配成功返回1……
重复上述过程,直到S的结尾,这时返回-1,说明匹配失败。
代码如下:
int match_1(const char * s, const char *p)
{
int slen=strlen(s),plen=strlen(p);
int i=0,j=0;
while(i<slen)
{
while(j<plen &&(i+j)<slen && s[i+j]==p[j]) j++;
if(j==plen) return i;
i++;
j=0;
}
return -1;
}
这段代码运行良好,但是为了对比,将其改为如下形式:
int match_2(const char * s, const char *p)
{
int slen=strlen(s),plen=strlen(p);
int i=0,j=0;
while(i<slen && j<plen)
{
if(s[i]==p[j])///匹配
{
i++;
j++;
}
else///不匹配了
{
i = i-j+1;///回退S的指针
j = 0;///P从头开始
}
}
if(j==plen) return i-j;///匹配成功
else return -1;
}
简单的说,就是i,j初始时分别指向s和p的起始位置,匹配时i,j同时向后移动,失配时i指针回退,j指针置零。
对于以上代码,你应当完全理解,给你笔和纸你就能准确无误的立刻写出来。这样你就对其工作过程了如指掌,否则后面的内容将难于理解。
复杂度分析:在最坏情况下,p每次都匹配到倒数第二个字符,然后最后一个字符不匹配,这时从头再来,显然每次p匹配都需要O(m)(其中m=strlen(p)),对s中的每个字符都匹配一遍,也需要O(n)(其中n=strlen(s)),总的复杂度就是O(mn)。
例如s=0000000000001,p=00001时,m=strlen(p)=5,n=strlen(s)=13.对于i∈[0-12],j∈[0-4],每次匹配到p[4]时,发现s[i]!=p[j]不匹配,都需要从头开始(j=0,i=i-j+1)下次匹配,即所谓的“回溯”。
我们发现,在这个过程中其实有很多回溯过程是没有必要的。比如:
i 0123456
s 0000000000001
p 00001
j 01234
在发现s[4]!=p[4]时,下次比较将从s[1]和p[0]比较开始:
i 0123456
s 0000000000001
p 00001
j 01234
一直比较到s[5]!=p[4]。其实如果注意到,在上一轮比较中,s[4]!=p[4]时(i=4,j=4),已经有
s[0...3]=p[0...3] ①
所以有
s[1...3]=p[1...3] ②
而
p[1...3]=p[0...2] ③
③中,p[0...2]为p的前缀,p[1...3]为后缀,这是由p本身的性质决定的,由②和③可以得到
s[1...3]=p[0...2] ④
看到这里你有什么感觉?既然s[1...3]=p[0...2]了,那为什么下一轮匹配过程还要从s[1]和p[0]开始呢?!直接比较s[4]和p[3](即s[i]和p[k],k=3)就可以了。kmp算法就是基于这个原理的,即发生失配后不需要每次都从p串的起始位置开始新的一轮比较(即i指针回退、j指针置零)。
上面这段分析,尤其是这三个等式,最好理解透彻。这里没有用各种ijk,而是用实际的字符串和数字表示,就是为了便于理解。理解透彻之后,就开始正式讲解kmp算法了。
3、kmp算法
kmp算法对普通字符串匹配算法(即match_2)的改进之处在于:发生失配之后,i指针不动,j指针指向合适的位置,开始下一轮的匹配。
假设合适的位置已经保存在一个数组next[n]中,n=strlen(p)。那么kmp的代码就是:
int match_3(const char * s, const char *p)
{
int slen=strlen(s),plen=strlen(p);
int i=0,j=0;
while(i<slen && j<plen)
{
if(s[i]==p[j])///匹配
{
i++;
j++;
}
else///不匹配了
{
//i =i-j+1; i指针不变,不再回退
j = next[j];///j指针从某个位置开始
}
}
if(j==plen) return i-j;///匹配成功
else return -1;
}
发生改变的地方就在else里:i指针不再回退,j指针不是归零而是某个特定的值。看上去是不是比朴素匹配算法更简单?貌似是的,但是问题是这个特定值是什么?也就是说这个next数组怎么求呢?好,现在来说这个next数组。
从第二部分的最后面的分析(就是①②③那三个式子那)我们知道,当s[i]!=p[j]时(失配了),已经匹配的部分为
s[i-j…i-1]=p[0…j-1] ④
即s[i]和p[j]的前j-1个字符。
如果想从p的第k个字符开始下一轮的匹配而不是回溯,那么必须要满足:p的前k-1个字符和s[i]前面的k-1个字符匹配(即k=next[j])。
p的前k-1个字符为: p[0…k-1]
s[i]的前k-1个字符为:s[i-k…i-1]
也就是
p[0…k-1]=s[i-k…i-1] ⑤
由④可得:
s[i-k…i-1]=p[j-k…j-1] ⑥
即s[i-j…i-1]和后k-1个字符和p的后k-1个字符相等。
由⑤⑥得
p[0…k-1]=p[j-k…j-1] ⑦
如果第一次看到这里,可能就有点晕了。我们再回顾下我们的问题。在p[j]处发生失配(s[i]!=p[j])后,i不动,j不需要置零,而是从k=next[j]处取得,那么这个k要满足⑦。
⑦是什么意思呢?左面是p的前k个字符(前缀),右面是p[0…j-1]的后k个字符(后缀)。这个k越大越好,为什么?因为越大j向后滑动的越多,效率越高。但是必须有k<=j-1 => k<j.
为便于理解,举个例子,对于p=”000011”:
j=0 ?
j=1 p[1]前有p[0]=p[0], next[1]=0
j=2 p[0-1]=p[0-1], next[2]=1
j=3 p[0-2]=p[0-2], next[3]=2
j=4 p[0-3]=p[0-3], next[4]=3
j=5 p[0…4]没有前缀=后缀, next[5]=0
j=0时怎么办呢?考虑最原始的情况,s[i]!=p[0]了,第一个就不匹配,当然没法跳了,开始下一轮即可。所以这里可以设定一个特殊值,表示第一个(j=0)就不匹配,一般设置为-1,可以简化程序。在j=-1时,需要进行下一轮匹配(i++;j++)j++后刚好是0。如果你一定要设置为-9,那么你要判断,当j==-9时,下一轮要:(i++; j=0;)。你还会注意到,对任何长度大于2的p串,next[1]==0是恒成立的。
再来一个例子:
p[]=”abababacd”的next数组next[9]:
j=0,k=next[0]=-1;
j=1,k=next[1]=0
j=2,p[2]之前有”ab”,其前缀只有’a’!=后缀’b’,所以k=next[2]=0
j=3,p[3]前为”aba”,p[0]=p[2], k=next[3]=1
j=4,p[4]前为”abab”,p[0…1]=p[2…3], k=next[4]=2
j=5,p[5]前为ababa,p[0…2]=p[2…4], k=next[5]=3
j=6,p[6]前为ababab,p[0…3]=p[2…5], k=next[6]=4
j=7,p[7]前为abababa,p[0…4]=p[2…6],k=next[7]=5
j=8,p[8]前为abababac,由于最后一个字母是c,任何前缀都没有和其相等的后缀,所以k=next[8]=0
相信通过这2个例子,你应该理解了next数组了吧。
由于next数组中存在j=-1的情况,所以我们的函数应该做少许修改:
int kmp(const char * s, const char *p)
{
int slen=strlen(s),plen=strlen(p);
int *next = (int*)malloc(sizeof(int)*plen);
getNext(p,next);
int i=0,j=0;
while(i<slen && j<plen)
{
if(j==-1|| s[i]==p[j])///j=-1时,也是进行下一轮匹配
{
i++;
j++;
}
else
{
j = next[j];///i,j不再回溯
}
}
free(next);
if(j==plen) return i-j;
else return -1;
}
我把函数名也修改为kmp,这也是kmp算法的最终形式(未优化的,后面再说优化的情况)。总共不到10行。
其中的getNext()函数就是计算next数组的,如果你理解了next数组的计算方法,你可以动手写一下试试,其代码也不过十行,而且和上面的kmp函数惊人的相似。至于getNext()的思路和具体代码,还在留给下一篇blog吧。
原创文章,转载请注明出处:http://blog.csdn.net/fastsort/article/details/9903153