算法—KMP字符串匹配
现在有一个问题,要从一个字符串中查找出指定子串的位置(初始下标),通常地,我们会使用朴素的字符串匹配算法,如下面这道题
给出主串和需要查找的子串,输出子串是否存在,并输出子串的首位在主串中的下标
- 首先,在主串中设下标i,在字串中设下标j,均从0开始
这时,将子串第一位与主串的第一位进行比较,结果是不同的,那么尽然不同,子串初始位置就一定不在当前的i处,所以i++,j还是0
i=1时,主串当前位置字符仍然与子串中的第一位不同,由上可得,i继续加一,j还是0
i=2时,匹配成功,这时i++,j++,继续判断下一位是否还能匹配
i=3时,仍旧匹配成功,重复上述操作
i=4,仍旧匹配。这时我们发现,i还没有到达主串的末尾,但是j已经到达子串的末尾。这就是查找成功的标志,返回i-j=2就是子串起始在主串中的下标
- 下面是这种朴素字符串匹配算法的代码
#include<stdio.h>
#include<string.h>
void search(char str[], char son[],int pos)
{
int index;
int i = pos; //主串中的位置下标
int j = 0; //字串中的位置下标
int len1 = strlen(str);
int len2 = strlen(son);
while(i < len1 && j < len2)
{
if(str[i] == son[j])
{
i++;
j++;
}
else
{
i = i - j + 1;
j = 0;
}
}
if(j = len2)
{
if(pos >= len1-len2 || i >= len1)
{
printf("不存在子串了\n");
return;
}
printf("查找到子串,起始索引在主串的%d处\n",i-len2);
search(str,son,i-len2+1); //递归继续搜索
}
}
int main()
{
char str[1000];
char son[1000];
gets(str);
gets(son);
search(str,son,0);
return 0;
}
上面的字符串查找确实简单易懂,但有时它却有着很大的缺点,我们看下面的这个例子
- 现在我们要从上面的主串中查找相对应的子串,我们按之前的朴素字符串查找过一遍
在 i=5,j=5时,判断字符不相符,这时,我们的i就需要回溯到i=1(第一个位置),j需要回溯到j=0(子串起始),然后继续开始如下的几个判断
- 这个时候你会发现,i居然又回到了5,而且字符匹配也没有丝毫进展!那么上面做的那几步岂不是无用功?为什么这么说呢?
我们仔细观察,子串的每一位的字符都是不同的,所以在第一次匹配到i=5,j=5
时,我们是已经知道主串的前五位是和子串的前五位相同的(也就是说主串的前五位也各不相同),所以中间的这四次比较都是可以省略的,我们可以直接跳到最后一步。
有人说这个例子太特殊? 那我们下面再看一个例子
我们继续使用朴素字符串查找
i=j=5时停下,i回溯到i=1,j回溯到0
- 这次我们又发现了什么? i又回到了5,j停留在2。在第一次比较到i=j=5时,我们已经知道主串和子串的前五位相同,也就是说,我们知道了主串的前三位各不相同,但主串的第一位和第四位相同,第二位和第五位相同,那么其实这中间的几步都是可以省略的,我们可以直接让i=5,j=2.
综上,我们发现主串中的i值是在不断回溯的,但最终又会回到原来的值,那么我们干脆不让它回溯了,即当判断主串与子串字符不同时,不改变i的值。
既然i值不回溯,也就是不可以变小,那么要考虑变化的就是j值了。通过观察也可以发现,我们屡次提到了字串的首字符与自身后面字符的比较,发现如果有相同字符,j值的变化就会不同,其实这个j值的变化与主串没关系,关键取决于子串的结构中是否有重复的问题。
- 在上面的第一个例子中,子串="abcdex"时,当中没有任何相同的字符,所以j就由5变成了0。第二个例子中,子串=“abcabx”,前缀的 “ab” 与后面的 "ab"是重复的,j就由5变成了2。因此我们可以得出规律:j值的的多少取决于当前字符之前的串的前后缀的相似度
现在就是整个KMP算法最核心的地方了,我们把子串各个位置的j值变化定义为一个数组next,那么next的长度就是子串的长度。我们有这样的函数定义:
- 当j=0时,next[j]=0
- 当{k| 1 < k < j 且 ‘p(1) … p(k-1)’ = ‘p(j-k+1) … p(j-1)’}此集合不为空时,next[j]=Max(k)
- 其他情况,next[j]=1
接下来,我们使用KMP算法的时候,只需要不让i回溯,通过求得子串的next数组,确定每个位置应该回溯到的j值,就能大大提高查找效率了,我们以上面的两个题为例子
子串=“abcdex”
- j=0时:next[0]=0
- j=1时:next[1]=1
- j=2时:2之前的串中没有对称前后缀,next[2]=1
- j=3时:同上
- j=4时:同上
- j=5时:同上
最终next数组={0,1,1,1,1,1};
这次,当我们判断到这一步时,
i不变,令j回溯到(next[j]-1),即i=5,j=0,我们直接来到了这一步:
第二个例子:
子串=“abcabx”
- j=0时:next[j]=0
- j=1时:next[j]=1
- j=2时:2之前的串中无对称的前后缀,next[2]=1
- j=3时:3之前的串中无对称的前后缀,next[3]=1
- j=4时:4之前的串中,第一位 'a’和第四位 'a’相同,故next[4]=2
- j=5时:5之前的串中,前两位 "ab"和后两位 "ab"相同,故next[5]=3
最终next数组={0,1,1,1,2,3}
那么我们在第一次判断后:
遵守KMP算法的规则,i不回溯,即i仍然等于5,j这时等于5,则应该回溯到(next[j]-1)=2处,我们就直接到了这一步:
是不是又直接跳过了中间的几步? 的确很神奇,这大大提高了字符串查找的效率
下面是KMP字符串匹配算法的代码:
#include<stdio.h>
#include<string.h>
void get_next(char son[],int next[])
{
int i,j;
int len = strlen(son);
i = 0;
j = -1;
next[0]=0;
while(i < len)
{
if(j == -1 || son[i] == son[j])
{
i++;
j++;
next[i] = j + 1;
}
else
j = next[j] -1; //j回退到合适的位置,i不变
}
}
void KMP(char str[],char son[],int pos)
{
int i = pos; //主串中的位置索引
int j =-1; //子串中的位置索引
int len1 = strlen(str);
int len2 = strlen(son);
int next[1000]; //next数组
get_next(son,next);
while(i < len1 && j <len2)
{
if(j == -1 || str[i] == son[j])
{
i++;
j++;
}
else
{
j = next[j] - 1;
}
}
if(j = len2)
{
if(pos >= len1-len2 || i >= len1)
{
printf("不存在子串了\n");
return;
}
printf("查找到子串,起始索引在主串的%d处\n",i-len2);
KMP(str,son,i-len2+1);
}
}
int main()
{
char str[1000];
char son[1000];
gets(str);
gets(son);
KMP(str,son,0);
return 0;
}
按理说这里应该结束了,但KMP算法并不是完美的,比如下面这种情况:
子串=“aaaaax”
- j=0时:next[0]=0
- j=1时:next[1]=1
- j=2时:'a’与 'a’对称 , next[2]=2
- j=3时: “aa” 与"aa"对称 next[3]=3
- j=4时:“aaa” 与"aaa"对称, next[4]=4
- j=5时:“aaaa” 与"aaaa"对称, next[5]=5
next数组为:{0,1,2,3,4,5}
然后i不变,令j根据所求的next数组回溯
我们发现,j又回到了0,我们知道子串中的前五位是相同的,那我们为什么还要一次次的将其与主串中的 'b’进行比较呢? 所以我们还需要对KMP算法进行优化
- 上面的例子中,我们可以直接来到最后一部,即i=5,j=0.所以在next数组中,第二三四五位的next值应该与第一位的next值相等。因此我们需要对求next数组的算法进行优化。
规则:在计算出next数组的同时,如果a字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值。如果不等,则该a位的nextval值就是它自己a位的next值
以上面的这个题为例
next数组为:{0,1,2,3,4,5}
- j=0时:nextval[0]=0
- j=1时:next指向j=0,串中j=1与j=0位置的字符相同,nextval[1]=nextval[0]=0
- j=2时:next指向j=1,串中j=2与j=1位置的字符相同,nextval[2]=nextval[1]=0
- j=3时:next指向j=2,串中j=3与j=2位置的字符相同,nextval[3]=nextval[2]=0
- j=4时:next指向j=3,串中j=4与j=3位置的字符相同,nextval[4]=nextval[3]=0
- j=5时:next指向j=4,串中j=5与j=4位置的字符不同,nextval[5]=next[5]=5
这时直接回溯到j=-1,然后
下面是优化过的KMP算法的代码:
#include<stdio.h>
#include<string.h>
void get_nextval(char son[],int nextval[])
{
int i,j;
int len = strlen(son);
i = 0;
j = -1;
nextval[0] = 0;
while(i < len)
{
if(j == -1 || son[i] == son[j])
{
i++;
j++;
if(son[i] != son[j]) //改进部位:若当前字符与前缀字符不同
{
nextval[i] = j + 1; //则当前的j为nextval在i位置的值
}
else
nextval[i] = nextval[j]; //如果与前缀字符相同,则将前缀字符的nextval值赋值给nextval在i位置的值
}
else
j= nextval[j] -1;
}
}
void KMP(char str[],char son[],int pos)
{
int i = pos; //主串中的位置索引
int j =-1; //子串中的位置索引
int len1 = strlen(str);
int len2 = strlen(son);
int next[1000]; //next数组
get_nextval(son,next);
while(i < len1 && j <len2)
{
if(j == -1 || str[i] == son[j])
{
i++;
j++;
}
else
{
j = next[j] - 1;
}
}
if(j = len2)
{
if(pos >= len1-len2 || i >= len1)
{
printf("不存在子串了\n");
return;
}
printf("查找到子串,起始索引在主串的%d处\n",i-len2);
KMP(str,son,i-len2+1);
}
}
int main()
{
char str[1000];
char son[1000];
gets(str);
gets(son);
KMP(str,son,0);
return 0;
}