【学习笔记】图解,以初学者角度简单易懂地理解kmp算法(C语言实现)

笔者的叨叨:这几天花了好些时间去学习kmp算法。可能由于我是一名初学者,理解起来比较费劲。但是不得不吐槽的一点就是:有关kmp算法的讲解,无论是我现有书籍也好,大佬们的博客也罢,概念说法不一,作为初学者是极容易绕进去的。所以在查阅各路大佬们的资料及结合自己的理解下,我希望将此算法作为笔记记录下来,方便自己以日后作为参考,同时希望各位初学者能通过这篇笔记少走弯路。

↓关于kmp算法学习笔记我会以一下纲要一一记录↓

纲要(阅读该笔记,你将学到:)

  • 了解kmp算法
    1.为什么kmp算法要比BF算法更好?
    2.kmp算法的工作原理是怎样的?
    3.next数组是怎样创建的?
    4.代码实现

了解kmp算法:

1. 为什么kmp算法要比BF算法更好?

A:为什么kmp算法要比BF算法更好?
Q:首先我们需要简单回顾一下BF算法

我们以一个例子为例:(初学者可以把目标串理解为源文本,模式串理解为待查询的文本。)

当一个目标串S(以下简称S串):“acdacaacdacc”与一个模式串T(以下简称T串):“acdacc”进行匹配时,如下图所示:

图1.(情况a)

图1
我们可以看到,开始的时候,T串j从0遍历到4的时候,与S串是一一匹配,此时只要S[5]和T[5]也匹配的话,那么T串和S串就匹配成功了。
但很可惜,T[5]!=S[5]。。。。。。
我们把这种情况称为情况a

图2.
图2:情况a:T[5]!=S[5]。。。。。。

来到这一步,按照BF算法的逻辑,那么下一步将是将T串向后移动一个单位,将T[0]对准S[1]继续遍历。

图3.
图3:T[0]!=[1],T向后移动一格
很显然T[0]!=[1],T向后移动一格。
按照这种算法如此进行,只要有与之相匹配的文本,最后都能完成匹配。

------可是面对情况a,按照kmp算法运行却是这样的(不懂也没关系,后面会详细说明。)
图4.
图2:情况a:T[5]!=S[5]。。。。。。
图4:S[5]!=T[2]

可见,根据kmp算法,在发生情况a时,模式串T向后移动了两个单位,同时i的位置不动,S[5]继续与t[2]进行匹配。

通过图三和图四的对比我们不难看出,使用kmp算法可以减少不必要的回溯,从而提高匹配效率。


2. kmp算法的工作原理是怎样的?

还是从情况a开始讲起,当发生情况a时,一个kmp算法使用的牛鼻哄哄的数组出现了:next数组。

A:next数组是啥啊?
Q:next数组的模式串T配套的数组,next数组中的数据决定着:当模式串T发生失配时,模式串T需要向后移动几个单位。

举个栗子:模式串T对应的next数组长这样:

图5.
图5:next数组

图2:情况a:T[5]!=S[5]。。。。。。
如上图所示:当T[5]和S[5]不匹配(情况a)时,此时j=5,因为j-1对应next数组的值为2,所以此时j值由原来的5变为现在的2,相当于T串向后移动了3位。
图6
可是移动完后我们发现,T[2]仍旧和S[5]不匹配,于是我们继续求组next数组,因为j-1对应next数组的值为0,所以此时j值由原来的2变为现在的0,相当于T串向后移动了2位。

此时,情况b出现了:

图6.
图7:S[5]!=T[0]
图5:next数组
由图6我们可以看到,在情况b中,S[5]和T[0]是不匹配的,如果按照情况a的处理方式,会发生j-1<0,也就是说,我们根本找不到j-1对应的next数组的值。

但是我们发现,此时j的值等于0,我们大可不必查找next数组的值,只需要把T串向后移动一个单位即可。

图7.图8:T串向后移动一个单位

到这儿T串总算是找到他的归属啦!

我们来小结一下吧:
1.当发生失配的时候:
情况a:只要j>0且发生失配,就一直执行j=next[j-1]的操作,循环如此。
情况b:当j=0,i++即可;
2.若当前字符匹配成功:
i++,j++即可


3. next数组是怎样创建的?

首先我们要了解什么是前缀和后缀:

Q:究竟是什么呀?
A:好吃的(大雾)

前缀:除了最后一个字符以外,一个字符串的全部从头部开始的子串组合
后缀:除了第一个字符以外,一个字符串的全部从尾部结束的子串组合

我们还是以T串(acdacc)为例,那么T串的前后缀分别是:

前缀:a,ac,acd,acda,acdac
后缀:c,cc,acc,dacc,cdacc

知道了前后缀,我们来了解公共前后缀:

Q:这又是什么呀?
A:好吃的(大雾)

公共前后缀应该顾名思义了吧,就是前缀和后缀一毛一样
就拿aabaa来说吧。
前缀:a,aa,aab,aaba
后缀:a,aa,baa,abaa
那么公共前后缀就有两个了,分别是“a”和,“aa”

我们可以看到,上面例子得到的公共前后缀的最大长度是2,所得的数值便是aabaa字符串中最后一个a对应的next数组的值了。

next值=公共前后缀的最大长度值

至此,如果看懂了,相信你一定有办法写一个创建next数组的函数了。
暴力遍历总该会吧。。。。。

需要注意的是,关于next数组的创建不同的人有不同的标准,比如有的人会将next[0]设置为0,而将得到的公共前后缀最大长度的值+1后写入next数组,这样做的目的是当读到next数组中的值为0时,跳出while循环(这段不懂也没关系)。当然这个想法和笔者的想法有些出入,但最终目的还是相同的,这里我们就不展开讨论了。

另外需要注意的是:以下讨论的next值都是公共前后缀的最大长度的值,而不是其+1的值。


进阶:递归法求next值:

还是拿aabaa来说吧。
当我们求最后一个a对应的next数组的时候,你是怎么求的呢?
还是遍历?这样的话如果T串很大的话岂不是很费劲?

A:那你有什么好方法嘛!
Q:我们就拿abcdabc abcdabc x(x为任意字符,具体字符如图所示)作例子来说吧!

图8.(情况c:当x为a时)
图8:当x为a时
从图9(情况c:当x为a时)中我们可以看出:j=13时对应的next值是7,此时我们比较一下T[7]T[14],发现他们相等!
故此时next[14]的值为7+1=8

规则1:如果当前位置j对应的next值为k,下一个位置j+1的字符与next[k]相等,那么next[j+1]=k+1

接下来我们看看另一种情况
图9.(情况d:当x为d时)
图9:(情况d:当x为d时)
与 情况c(当x为a时)不同,此时x的字符,也就是T[14]的字符为d,此时我们比较一下T[7]T[14],发现他们不相等!

那怎么办呢?重新遍历?
nonono!

读者可以观察一下这四个用红框框住的子串,有没有一点发现?
对!又是公共前后缀!

也就是说,在原来的前(后)缀里面,我们有可能还能找到一个最长公共前后缀!
而根据简单的等价代换(由n1=n2可得m1=m3,由m3=m4可得m1=m4)我们又找到了第二长的公共前后缀!

Q:那么怎么确定第二长的公共前后缀的长度呢?
A:len=next[next[j-1]]

你品,你品,你细品,细心的同学发现了,这就是一个递归。
而这个递归的结束条件是:最后一次next的值为0


4.代码实现

#include<stdio.h>
#include<string.h> 
#include<malloc.h> 

int getlocation(char* s1, char* s2, int* next)
{
	int len1, len2, i, j = 0;
	len1 = strlen(s1);
	len2 = strlen(s2);
	for (i = 0; i < len1; i++)
	{
		if (s1[i] == s2[j])
			j++;
		else
		{
			if (j > 0)
			{
				j = next[j - 1];
				--i;
			}
		}
		if (j == len2)
			return i - len2+2;
	}
}

int* getnext(char* s)
{
	int* next = NULL;
	int len, i;
	len = strlen(s);
	next = (int*)malloc(sizeof(int) * len);
	next[0] = 0;
	for (i = 1; i < len; i++)
	{
		int k = next[i - 1];
		if (k > 0)
		{
			if (s[i] == s[k])
				next[i] = 1 + k;
			else
			{
				while (k > 0)
				{
					k = next[k - 1];
					if (s[i] == s[k])
					{
						next[i] = 1 + k;
						break;
					}
				}
				if (k <= 0)
				{
					if (s[i] == s[0])
						next[i] = 1;
					else next[i] = 0;
				}
			}
		}
		else
		{
			if (s[i] == s[0])
				next[i] = 1;
			else next[i] = 0;
		}
	}
	return next;
}
int main()
{
	char s1[100], s2[20];
	int* next = NULL, location;
	printf("请分别输入源文本和待查询文本:\n");
	scanf_s("%s%s", s1,20, s2,20);
	//获取next数组 
	next = getnext(s2);
	// 获取文本相匹配的位置 
	location = getlocation(s1, s2, next);
	if (location)
		printf("你要查询的文本在第%d个字符处出现呢~\n", location);
	else printf("oops,好像找不到你要找的文本~\n");
	return 0;
}

笔记到这里就结束了,
各位看官如果有收获的话
不妨点个赞呗~~~~~~~~~~~~
from:一个还在为数据结构与算法挣扎的学生

部分内容参考:

  1. 字符串匹配的kmp算法
  2. KMP算法的前缀next数组最通俗的解释,如果看不懂我也没辙了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值