一、简介
上一篇介绍了标准的 KMP 模式匹配算法,但是这种算法在某种情况下存在一定的缺陷,会产生不必要的比较。
改进后的 KMP 模式匹配算法进行匹配时,和标准 KMP 算法的流程一样。仅仅在求取 next 数组的值时,如果子串该位置的字符与 next 数组指向的内容相等,则该位置的 next 数组值修改为所指向位置的 next 数组值。
二、案例分析
假设主串为 “aaaaadef”,子串为 “aaaaaf”,规定子串和主串的起始位置下标均为 1,主串的位置下标用 i 表示,子串的位置下标用 j 表示。标准 KMP 算法计算出来的 next[] = {0 1 2 3 4 5},当 i=6, j=6 时,主串的字符 “d” 与子串的 “f” 不相等,本次匹配失败。下一次匹配,i=6,j=next[j],j=5;主串与子串仍然不等…,直到 j=1 ,还是不等。我们可以发现,j 等于 1,2,3,4,5 的比较过程完成完全是多余的。
1、求子串 “aaaaf” 改进后的 next 数组值
(1)、标准 KMP 计算出来的 next 数组是 {0 1 2 3 4};
(2)、当 j = 1时,根据公式,next[1] = 0;
(3)、当 j = 2时,next 的值为 1,当前字符与第 1 个字符相等,故next[2]=next[1]=0;
(4)、当 j = 3时,next 的值为 2,当前字符与第 2 个字符相等,故next[3]=next[2]=0;
(5)、当 j = 4时,next 的值为 3,当前字符与第 3 个字符相等,故next[4]=next[3]=0;
(6)、当 j = 5时,next 的值为 4,当前字符与第 4个字符不等,故next[5]=4;
所以改进后的 KMP 算法计算出来的 next 数组为 {0 0 0 0 4}。
2、求子串 “ababaaaba” 改进后的 next 数组
(1)、标准 KMP 计算出来的 next 数组是 {0 1 1 2 3 4 2 2 3};
(2)、当 j = 1时,根据公式,next[1] = 0;
(3)、当 j = 2时,next 的值为 1,当前字符与第 1 个字符不等,故next[2]=1;
(4)、当 j = 3时,next 的值为 1,当前字符与第 1 个字符相等,故next[3]=next[1]=0;
(5)、当 j = 4时,next 的值为 2,当前字符与第 2 个字符相等,故next[4]=next[2]=1;
(6)、当 j = 5时,next 的值为 3,当前字符与第 3个字符相等,故next[5]=next[3]=0;
(7)、当 j = 6时,next 的值为 4,当前字符与第 4 个字符不等,故next[6]=4;
(8)、当 j = 7时,next 的值为 2,当前字符与第 2 个字符不等,故next[7]=2;
(9)、当 j = 8时,next 的值为 2,当前字符与第 2 个字符相等,故next[8]=next[2]=1;
(10)、当 j = 9时,next 的值为 3,当前字符与第 3个字符相等,故next[9]=next[3]=0;
所以改进后的 KMP 算法计算出来的 next 数组为 {0 1 0 1 0 4 2 1 0}。
三、程序实现
注意:经过如下算法计算出来的 next 数值内容比理论计算出来的 next 数值内容小 1 ,这是因为理论计算中,从 1 开始计算,而这种计算方法是从 0 开始计算的,相对来说减少了存储空间,但是不便于理解,客观的说这种算法比较牵强,更细致明白的方法请参考《大话数据结构》。
//改进后的 KMP 模式匹配算法
//设计思想来自于《大话数据结构》
#include <stdio.h>
#define T_LENGTH 9
#define S_LENGTH 20
void GetNext(char string[], int next[]);
int GetIndex(char s[], char t[], int next[]);
int main(){
int next[T_LENGTH] = {};
char s[S_LENGTH] = "eababaaabaeeeeeeee";
char t[T_LENGTH] = "ababaaaba";
GetNext(t, next);
printf("%d ",GetIndex(s, t, next));
return 0;
}
//获取 string 子串的标准 KMP 算法下的 next 数组
//next 数组的长度为 T_LENGTH,从 0 开始存储
//string 的长度为 T_LENGTH
void GetNext(char string[], int next[]){
int i = 0; // i 位置代表 string 字符串的当前位置下标
int j = -1; // j 表示需要回溯的位置
// 从 -1 开始是和后面 if 判断保持一致
next[0] = -1; //根据公式,首字符的 next 值为 0。但是此处为 -1 为了和后面保持一致
while(i < T_LENGTH){
// j自增一次后才是正确初始位置 0
if((j == -1) || (string[i] == string[j])){
++i;
++j;
if(string[i] != string[j]){
next[i] = j;
}else{
next[i] = next[j];
}
}else{
j = next[j];
}
}
// for(i = 0; i < T_LENGTH; ++i){
// printf("%d ",next[i]);
// }printf("\n\n");
}
//返回子串 t 在主串 s 中的位置索引,如果没有,则返回 -1
int GetIndex(char s[], char t[], int next[]){
int i = 0;
int j = -1;
while(i < S_LENGTH-1 && j < T_LENGTH-1){
if(j == -1 || s[i] == t[j]){
++i;
++j;
}else{
j = next[j];
}
}
printf("j = %d daf\n",j);
if(j >= T_LENGTH -1){
return i - T_LENGTH + 1;
}else{
return -1;
}
}