先来看朴素匹配模式:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXSIZE 256
int index(int pos,char *S,char *T)
{
int i=0;
int j=0;
int len1 = strlen(S);
int len2 = strlen(T);
while(i < len1 && j < len2)
{
if(S[i] == T[j])
{
i++;
j++;
}
else
{
i = i-j+1;
j = 0;
}
}
if(j == len2)
{
i = i-j+1;
return i;
}
else
return -1;
}
char *s_gets(char *str,int n)
{
char *pt;
pt = fgets(str,n,stdin);
if(pt)
{
while(*str && *str !='\n')
str++;
if(*str == '\n')
*str = '\0';
else
while(getchar() != '\n')
continue;
}
return pt;
}
int main()
{
char S[MAXSIZE];
char T[MAXSIZE];
char *pt;
int pos,find;
printf("请输入主串(eof or enter to quit):");
while(s_gets(S,MAXSIZE) != NULL && S[0] != '\0')
{
printf("请输入子串:");
pt = s_gets(T,MAXSIZE);
if(pt == NULL)
{
puts("拜拜!");
exit(0);
}
printf("请输入在主串中查找的位置:");
scanf("%d",&pos);
while(getchar() != '\n')
continue;
if((find = index(pos,S,T)) != -1)
{
printf("子串在%d位置之后第一次出现的位置为:%d\n",pos,find);
}
else
puts("无法寻得");
system("PAUSE");
system("CLS");
printf("请输入主串(eof or enter to quit):");
}
puts("拜拜!");
return 0;
}
很简单的算法:选定开头位置去遍历主串中的各个位置,遇到和子串不同就从下一个位置开始,继续遍历知道达到找到子串的条件或者已经没有下个位置可以遍历为止。
再来看看KPM的改进版
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXSIZE 256
void get_next(char *str,int *next)
{
int len = strlen(str);
int i = 0;
int j = -1;
next[0] = -1;
while(i < len-1) //每次是给后面的位置求值
{
if(j == -1 || str[i] == str[j])
{
i++;
j++;
if(str[i] != str[j])
next[i] = j; //不需要向前寻找最大前缀
else
next[i] = next[j]; //寻找最前的最大前缀
}
else
j = next[j];
}
}
int KPM_index(int pos,char *S,char *T) //S为主串,T为子串
{
int len1 = strlen(S);
int len2 = strlen(T);
int i = pos;
int j = 0;
int next[MAXSIZE];
get_next(T,next);
while(i < len1 && j < len2)
{
if(j == -1 || S[i] == T[j])
{
i++;
j++;
}
else
j = next[j];
}
if(j == len2)
{
i = i-j+1;
return i;
}
return -1;
}
char *s_gets(char *str,int n)
{
char *pt;
pt = fgets(str,n,stdin);
if(pt)
{
while(*str && *str !='\n')
str++;
if(*str == '\n')
*str = '\0';
else
while(getchar() != '\n')
continue;
}
return pt;
}
int main()
{
char S[MAXSIZE];
char T[MAXSIZE];
char *pt;
int pos,find;
printf("请输入主串(eof or enter to quit):");
while(s_gets(S,MAXSIZE) != NULL && S[0] != '\0')
{
printf("请输入子串:");
pt = s_gets(T,MAXSIZE);
if(pt == NULL)
{
puts("拜拜!");
exit(0);
}
printf("请输入在主串中查找的位置:");
scanf("%d",&pos);
while(getchar() != '\n')
continue;
if((find = KPM_index(pos,S,T)) != -1)
{
printf("子串在%d位置之后第一次出现的位置为:%d\n",pos,find);
}
else
puts("无法寻得");
system("PAUSE");
system("CLS");
printf("请输入主串(eof or enter to quit):");
}
puts("拜拜!");
return 0;
}
第一点: KPM中的i为什么不用回溯
子串再怎么千变万化,其实能归纳为两种情况:一种是子串里没有重复字符的,另一种是子串里有重复字符的。
没有重复字符的情况: 我在主串中遍历到位置i发现与子串此时的位置j不同,又因为子串中没有相同的字符,那i位置前面与子串中相同的字符自然与子串的开头不同,那么子串可以直接从位置i开始遍历而不用回溯到i的下一位再开始。
子串中存在字符重合的情况:这时候我们就要考虑前缀和后缀的问题。
前缀和后缀的概念:例如一个字符串“abcabx” 前缀为{“a”,“ab”,“abc”,“abca”,“abcab”},后缀为:{“b”,“bc”,“bca”,“bcab”,“bcabx”}
举例(运用上诉子串)当我们在主串位置i中找到位置5(字符串从0算起,即x)时遭遇不同,我们主串位置i不需要像朴素匹配模式那样回溯,j也不需要回溯到开头,我们只需要回溯到子串中的位置c,原因:到x位置相同可以看出子串中的子串“abcab”,前缀ab和后缀ab相同也就是说我在i位置前两个字符为ab与前缀abc中的ab相同那么我们只要回溯到c用c和主串位置i进行下次比较即可,如若还是不行我们再回溯,这回abc没有前后缀相同的字符了,只能回溯到0从0比起。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void get_next(int next[],char *str)
{
int i=0;
int j=-1;
next[0] = -1;
while(i < strlen(str)-1)
{
if(j == -1 || str[i] == str[j])
{
i++;
j++;
next[i] = j;
}
else
j = next[j];
}
}
int Index_KMP(int pos,char *T,char *S)
{
int i,j;
j = 0;
i = pos;
int next[1000];
get_next(next,T);
while(i < (int)strlen(S) && j < (int)strlen(T)) //需要强制转换strlen,若是没有强制转换j为-1时会结束循坏
{
if( j == -1 || S[i] == T[j])
{
i++;
j++;
}
else
{
j = next[j];
}
}
if(j == strlen(T))
return i-strlen(T);
else
return -1;
}
char *s_gets(char *str,int n)
{
char *pt;
pt=fgets(str,n,stdin);
if(pt)
{
while(*str && *str != '\n')
str++;
if(*str == '\n')
*str = '\0';
else
while(getchar()!='\n')
continue;
}
return pt;
}
int main()
{
char S[1000];
char T[1000];
int pos;
int find;
printf("请输入主字符串(EOF与开头回车退出):");
while(s_gets(S,1000) != NULL && S[0] != '\0')
{
printf("请输入子字符串:");
s_gets(T,1000);
printf("请输入从哪个位置开始查找:");
scanf("%d",&pos);
getchar();
printf("第一次出现子串的位置为:%d\n",Index_KMP(pos,T,S));
printf("请输入主字符串(EOF与开头回车退出):");
}
return 0;
}
第二点KPM普通版next的推导(上述图片字符串开头为1,0号位记录字符串长度,我们以普通字符串来说明: KPM普通匹配模式代码):
由于字符串的第一位和第二位没有前后缀,所以第一位的next为-1(防止死循环)和 0(再与开头比较)。当遇到相同时next[i] = j (记录下当前主串若在该子串位置遇到不同时该去哪里寻找拥有相同开头的子串位置),也蕴含着一种继承的概念,比如说我 0位 1位相同 那么next[2] = 1; 当主串在j=2时遇到不同那么子串就会回溯到与当前位置有相同开头的位置1,若子串中 2位 3位 依旧相同 next[3] = 2 此时next[3]就继承了next[2]的1变为2。同时你会发现其实每次i++,j++就是在创造新的前缀和后缀,只要新的前后缀相同就可以完成这种继承(当遇到不同就会寻找拥有相同初始位置的next【j】,直到与next[0]不同,j = -1,next【i】设为0,接下来的next[i]开始新的传承)。
第三点KPM改进版:
就是原本kpm的基础新增一个特性:寻找最初初始位置相同的那个j 比如说:
字符串:“a b a b a b a a b”
netx: -1 0 -1 0 -1 0 -1 5 0
1号b有next后:i = 1 j = 0
2号位:1和0不同,j回溯到-1 j++,i++,a[0] == a[2] 所以 next[2] = next[0] = -1;
3号位:0和2相同,j变为1,1和3相同 next[3] = 0
4号位: 3和1相同,j变为2,4和2相同 next = -1
5号位:4和2相同,j变为3,3和5相同 next = 0
6号位:5和3相同,j变为4,4和6相同 next = -1
7号位:6和4相同,j变为5,7和5不同
8号位:5和7不同,j变为0,,0和7相同,j变为1,而1又和8相同 next = 0
(这其实是一种特殊情况,因为每次连续相同就会寻找拥有相同前缀的最前位置,可位置1没有前缀所以只能寻找位置0导致这种情况的特殊,但也说明了这种算法能够应对所有情况)
正常例子:“aaaaaaaab”
next:000000008