笔者的叨叨:这几天花了好些时间去学习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)
我们可以看到,开始的时候,T串j从0遍历到4的时候,与S串是一一匹配,此时只要S[5]和T[5]也匹配的话,那么T串和S串就匹配成功了。
但很可惜,T[5]!=S[5]。。。。。。
我们把这种情况称为情况a
图2.
来到这一步,按照BF算法的逻辑,那么下一步将是将T串向后移动一个单位,将T[0]对准S[1]继续遍历。
图3.
很显然T[0]!=[1],T向后移动一格。
按照这种算法如此进行,只要有与之相匹配的文本,最后都能完成匹配。
------可是面对情况a,按照kmp算法运行却是这样的(不懂也没关系,后面会详细说明。)
图4.
可见,根据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.
如上图所示:当T[5]和S[5]不匹配(情况a)时,此时j=5,因为j-1对应next数组的值为2,所以此时j值由原来的5变为现在的2,相当于T串向后移动了3位。
可是移动完后我们发现,T[2]仍旧和S[5]不匹配,于是我们继续求组next数组,因为j-1对应next数组的值为0,所以此时j值由原来的2变为现在的0,相当于T串向后移动了2位。
此时,情况b出现了:
图6.
由图6我们可以看到,在情况b中,S[5]和T[0]是不匹配的,如果按照情况a的处理方式,会发生j-1<0,也就是说,我们根本找不到j-1对应的next数组的值。
但是我们发现,此时j的值等于0,我们大可不必查找next数组的值,只需要把T串向后移动一个单位即可。
图7.
到这儿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时)
从图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时)
与 情况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:一个还在为数据结构与算法挣扎的学生
部分内容参考: