1. 相关概念:
- 模式字符: 指的其实就是子字符串
2. 算法思想:
- 可以把字符匹配看作状态的转移, 如果匹配就进入下一状态, 如果不匹配就回溯
- 即状态转移由两部分因素决定, 当前状态与输入字符
- 如果提前预定好了状态转换表, 则匹配过程如下图所示:
- 所以最主要的工作是根据给定的字串生成对应的状态转换表
- 实际上KMP中只有两种状态转换:
- 如果输入字符 == 当前状态所代表的字串字符, 则向前推进
- 如果输入字符 != 当前状态所代表的字串字符, 则回溯
- 而其中最重要的就是确定回溯的位置
- 结论:如下图
- 回溯的位置 ⇒ X的下一步
- 跳到当前X记录所在状态, 并在此状态下, 按当前枚举的输入字符走一步, 下一步的位置就是回溯状态
- 而X记录的位置也是要维护的, 每次枚举完所有的输入字符, X都要在X状态上走一步, 按照最新状态所代表的字串字符来走
- 回溯的位置 ⇒ X的下一步
- 直观理解: 经过上述操作, X与j拥有相同的前缀, 且该前缀会尽可能的长
- 这样就能缩短回溯距离, 节省时间,
- 因为能匹配到j说明j的前缀能够匹配, 所以X的前缀也必定能匹配, 省去从头匹配前缀
3. 在c语言中的实现
梗概:
- 生成状态转换表:
- 第一个状态直接初始化为 字符符合往下走 字符不符合默认跳转到0状态
- X初始在0状态
- 从第二个状态开始枚举所以可能的输入字符 代码:
int KMPpat(char pat[],int ***dp){
int len = strlen(pat);
/*初始化状态转换表*/
(*dp)=(int**)calloc(len,sizeof(int*));
for(int j=0;j<len;++j){
(*dp)[j]=(int*)calloc(256,sizeof(int));
for(int i=0;i<256;++i)
(*dp)[j][i]=0;
}
if(!*dp)return 0;//申请空间失败
// dp[当前状态][传入字符]=转跳的状态
/*初始化X和状态0*/
(*dp)[0][pat[0]]=1;
int X=0;
for(int j=1;j<len;++j){
for(int c=0;c<256;++c)
/*传入字符不符合的话都要回溯*/
(*dp)[j][c]=(*dp)[X][c];
/*字符符合进入下一检测状态*/
(*dp)[j][pat[j]]=j+1;
/*X按当前j转跳状态*/
X=(*dp)[X][pat[j]];
}
return 1;
}
int KMPmatch(char pat[],char txt[]){
//以pat为子串匹配txt字符串,成功返回已匹配的开始位置
//无字串返回-1,空间不足返回-2
int **dp;
/*根据模式字符串生成对应的匹配状态转换表*/
if(!KMPpat(pat,&dp))return -2;
int patlen = strlen(pat);
int txtlen = strlen(txt);
int j=0;
for(int c=0;c < txtlen;++c){
/*根据dp转换当前匹配检测状态*/
j=dp[j][txt[c]];
if(j==patlen){//终点状态,匹配完毕
for(int i=0;i<patlen;++i)
free(dp[i]);
free(dp);
return c-patlen+1;
}
}
for(int i=0;i<patlen;++i)
free(dp[i]);
free(dp);
return -1;
}