KMP模式匹配算法(两种)

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;
}

以上就是全部内容了,我表达意思可能不到位,多多包涵~~谢谢你的观看!

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值