KMP模式匹配算法(两种)
具体可分为:前缀(prefix)法和next数组法
一、前缀(prefix)法:
1. 思路:
1)求出前缀(prefix)表 2)依照前缀(prefix)表来帮助匹配查找
所以,找出前缀(prefix)表是最核心的步骤
2. 方法:
给出两种求前缀(prefix)表的方法,这两种方法的核心大致相当,都利用回溯的思想
本例中用指针j进行回溯,所以我把它俩都称之为:j值回溯法,来计算前缀(prefix)表
1)求未成熟的前缀(prefix)表,将未成熟的表处理为成熟的前缀(prefix)表,加工处理过程为:prefix[]数组每个元素都后移一位(最后一个元素理所应当被覆盖),再将prefix[0]置为-1即可
以str[] = "ababax"为例:
前缀(prefix)表,代码如下:
void PrefixTable(void)//前缀表函数
{
char str[] = "ababax";
int i = 1, j = 0;
int n = strlen(str);
int *prefix = (int *)calloc(n, sizeof(int));
//未成熟前缀表求值开始
while (i < n)
{
if (str[i] == str[j])
{
j++;
prefix[i] = j;
i++;
}
else
{
if (j > 0)
j = prefix[j - 1];//j值回溯(左下)
else
{
prefix[i] = j;
i++;
}
}
}
//未成熟前缀表求值结束,接下来为处理环节
for (i = n - 1; i > 0; i--)
prefix[i] = prefix[i - 1];
prefix[0] = -1;
//处理完毕,输出成熟前缀表
for (i = 0; i < n; i++)
printf("%2d ", prefix[i]);
return;
}
2)方法1)改进,改变初始指针的位置,改变prefix[0]的值,使之从1)中的0变成2)中的-1
void Prefix_Table(void)
{
char str[] = "ababax";
int i = 0, j = -1;//方法1)中初始时 i=1,j=0
int n = strlen(str);
int *prefix = (int *)calloc(n, sizeof(int));
prefix[0] = -1;
//开始求成熟的前缀表
while (i < n - 1)
{
if (j == -1 || str[i] == str[j])
{
++i;
++j;
prefix[i] = j;
}
else
j = prefix[j];//若字符不相同,则j值回溯(正下)
}
//结束成熟前缀表的求值,因为是成熟的,不需要加工,接下来打印成熟前缀表
for (i = 0; i < n; i++)
printf("%2d ", prefix[i]);
return;
}
两个方法大致相当,只不过回溯略有区别,1)中回溯为左下回溯,2)中回溯为正下回溯,解释一下:
方法1):
下标:0 1 2 3 4 5 初始 i = 1, j = 0
字符:a b a b a x
prefix[]: 0 ? ?
prefix[0]初始化为0定义的时候,判断str[i]与str[j]是否相等,如果相等,那么立马就能将prefix[1]填入相应的值,然后 i 后移,再次判断,倘若str[i] 与str[j]不相等,就需要回溯j的值,j = prefix[j-1],意思就是,暂时得不到prefix[i]的值,把j所指元素的前一个的prefix[]值即prefix[j-1]赋给j,完成回溯,由于j和prefix[j-1]的位置关系(如图prefix[j-1]在j的左下位置),所以称其为左下回溯法
方法2):
下标:-1 0 1 2 3 4 5 初始 i = 0, j = -1
字符:空 a b a b a x
prefix[]: -1 ? ?
prefix[0]初始化为-1,判断if (j == -1 || str[i] == str[j]),str[-1]不存在,但是前面 j == -1成立时,后面就不计算了,所以无影响。这次如果if条件为假,那么将j回溯, j = prefix[j],由于j和prefix[j]的位置关系(如图prefix[j]在j的正下方),所以称之为正下回溯法
下面给出完全代码:
void KmpSearch(void)
{//采用方法1)求前缀表
char text[] = "ababaxjcash ababaxjcababaxlkdababclkcjkaababax";
char str[] = "ababax";
int i = 1, j = 0;
int m = strlen(text);
int n = strlen(str);
int *prefix = (int *)calloc(n, sizeof(int));
while (i < n)
{
if (str[i] == str[j])
{
j++;
prefix[i] = j;
i++;
}
else
{
if (j > 0)
j = prefix[j - 1];
else
{
prefix[i] = j;
i++;
}
}
}
for (i = n - 1; i > 0; i--)
prefix[i] = prefix[i - 1];
prefix[0] = -1;
for (i = 0; i < n; i++)
printf("%2d ", prefix[i]);
printf("\n");
//以上部分可完全用方法2)求前缀表来替换
j = 0; i = 0;
while (i < m)
{
if (j == n - 1 && text[i] == str[j])
{
printf("Found at %d !\n", i - j + 1);//+1是因为数组的0位为第一个数
j = prefix[j];
}
if (text[i] == str[j])
i++, j++;
else
{
j = prefix[j];
if (j == -1)
i++, j++;
}
}
for (i = 0; i < n; i++)
printf("%2d ", prefix[i]);
return;
}
总结:方法1)和方法2)由于指针i、j初始位置不同,prefix[0]不同,回溯方式不同,得到的前缀(prefix)表不同,方法2)直接一步算出,方法1)先算出未成熟的前缀(prefix)表,再加工而得到成熟的前缀(prefix)表,得到前缀(prefix)表后,按照上述操作可进行匹配查找
二、next数组法:
1. 思路:
1)求出next[]数组 2)利用next[]数组进行匹配查找
2. 方法:
1)这里的next[]数组,实际上数值对应关系上为prefix[]数组元素+1,举个例子:prefix[]: -1 0 0 1 1 2 0 1,那么next[]为:0 1 1 2 2 3 1 2,记住一点,对应关系为next[i]=prefix[i-1]+1,i>=1 直接利用上面的代码(回头看)进行+1操作
当然有更直接的方法:调节i,j指针,和字符数组存放位置,这种方法就是上面方法2)求前缀(prefix)表的改版,下面讲一讲具体操作:
2)初始i、j指针的位置:i = 1, j = 0; 因此字符在数组中的存放应该是这样,以str[] = "ababax"为例:
str[] : 0 1 2 3 4 5 6
字符 无 a b a b a x
str[0]不放字符,它只是一个用来回溯的出发点设定(j = 0),像程杰老师著的《大话数据结构》一书中,就把字符串的长度放在了str[0]中(原书是T[0]),所以代码即为上面方法2)的小改版:
void NextProces(void)
{
char str[] = " ababax";//1 ~ 6存放字符,空格占位作用
int i = 1, j = 0;
int n = strlen(str);
int *next = (int *)calloc(n, sizeof(int));//长度与str[]保持一致
while (i < n - 1)
{
if (j == 0 || str[i] == str[j])
{
++i;
++j;
next[i] = j;
}
else
j = next[j];//若字符不相同,则j值回溯
}
for (i = 1; i < n; i++)
printf("%2d ", next[i]);
return;
}
注:这种直接算next[]数组的方法和prefix[]+1算next的方法结果略有不同:主要是位置不一样,直接算next[]数组结果从next[1]next[6]有的6个数,而j用prefix[]+1算next[]是从next[0]next[5]的6个数
接下来,就是用next[]数组进行匹配查找
查找过程中,需要用到两个指针,分别是i和j,i指针为主串指针,j指针为子串指针,通过子串j值回溯,进行匹配查找,下面讲一讲next[]数组进行匹配查找的核心思路:
next[]数组与(成熟)前缀(prefix)表最大的区别有两点,也是我反复说的:
1. next[]数组第一位即next[0]不存放有效数字,实际上是从1开始,子串第一位str[0]也不存放有效字符,比如对于str[] = "ababax"和next[],实际上应该写为str[] = “Xababax”,因此得到的next[]数组也是从1开始,而前缀(prefix)表从0开始
2. 数字关系上,有效数字对应为:next[] = prefix[] + 1,不考虑有效性(下标)的情况下,为next[i] = prefix[i-1]+1,i从1开始
注:next[]数组法,在程杰所著《大话数据结构》中,子串str[](书中为T串)和主串text(书中为S串),均是数组下标从1开始的
程杰《数据结构》版总体代码如下(有改动):
void KmpSearch(void)
{
char text[] = " ababaxjcash ababaxjcababaxlkdababclkcjkaababax";//0位占位,不放字符
char str[] = " ababax";//0位占位,不放字符
int i = 1, j = 0;
int m = strlen(text);
int n = strlen(str);
int *next = (int *)calloc(n, sizeof(int));
while (i < n)
{
if (j == 0 || str[i] == str[j])
{
++i;
++j;
next[i] = j;
}
else
j = next[j];//若字符不相同,则j值回溯
}
j = 1, i = 0;//等价于j = i = 1;
while (i <= m)
{
if (j == n - 1 && text[i] == str[j])
{
printf("Found at %d !\n", i - j + 1);
j = 0;
}
if (j == 0 || text[i] == str[j])
{
++i;
++j;
}
else
j = next[j];
}
return;
}
下面聊一聊我的想法,我认为数组下标从1开始,不符合正常逻辑,所以我们还是将str[]数组、text[]数组下标从0开始,得到的next数组[]也从0开始存放值,由于前缀(prefix)表与next[]数组特殊关系,所以我用前缀(prefix)表改出了与上面新next[]数组,与原next[]数组不同的是,该新next[]数组从0开始存放数值,下面给出我的代码:
void Kmp_Search(void)
{
char text[] = "ababaxjcash ababaxjcababaxlkdababclkcjkaababax";
char str[] = "ababax";
int i = 0, j = -1;
int m = strlen(text);
int n = strlen(str);
int *next = (int *)calloc(n, sizeof(int));
while (i < n - 1)
{
if (j == -1 || str[i] == str[j])
{
++i, ++j;
next[i] = j;
}
else
j = next[j];//若字符不相同,则j值回溯(正下)
}
for (i = 0; i < n; i ++)
next[i] += 1;
while (i < m)
{
if (j == n - 1 && text[i] == str[j])
{
printf("Found at %d !\n", i - j + 1);
++i, j = 0;
}
if (text[i] == str[j])
++i, ++j;
else
{
if (j == 0) ++i;
else
j = next[j-1];
}
}
return;
}
以上就是全部内容了,我表达意思可能不到位,多多包涵~~谢谢你的观看!