目录
前言
在我们进行字符串匹配或者说模拟实现C语言库函数strstr()时,我们不难发现暴力求解( BF算法)的时间消耗是非常高的。于是Donald Knuth、Vaughan Pratt和James Morris经研究后于1977年提出了一个更简便的算法,也就是KMP算法。 ------2023.8.18记
在正文开始前,我们需要了解几个概念:前缀与后缀、最长相等前后缀、文本串、模式串
一·前缀、后缀、最长相等前后缀
我们在进行字符串匹配时候,题目会给出两个字符串(可以参考:leetcode上的一道题目28.找出字符串中第一个匹配项的下标),
模式串(Pattern):是指用来进行匹配的字符串,也可以称为待匹配字符串。它是我们需要在一个文本串中查找的目标字符串。
文本串(Text):是指要进行查找或匹配操作的字符串,也可以称为源字符串。它是我们需要在其中进行模式串匹配操作的字符串。
而我们讲的最长相等前后缀是对模式串而言的,因为KMP算法要求我们不回溯文本串。
以下图中的两个字符串为例,来解释上面几个概念
前缀(prefix):指的是一个字符串除了最后一个字符外的所有前缀子串(包括空串)
后缀(suffix):指的是一个字符串除了第一个字符外的所有后缀子串(包括空串)。
另外请注意:
对于前缀字符串:其是必须包含字符串首元素的、不包含末尾字符的、连续的、不包括原字符串本身的、所有子字符串。
对于后缀字符串:其是必须包含字符串尾元素的、不包含首字符的、连续的、不包括原字符串的、所有子字符串。
那么对于模式串我们可以分别求出其
前缀:“A”、“AB”、“ABA”、“ABAB”
后缀: "C"、“BC”、"ABC"、“BABC”
我们不难看出:此时,没有相等的前后缀。
二·kmp算法逻辑介绍
我们先用动图来演示一下
演示视频
(假设我们已经求出了next数组,关于next数组如何构造,我们稍后解释)
我们先对各个变量命个名,以方便解释
文本串text、模式串pattern、文本串长度len_text、模式串长度len_pattern
从中我们不难看出的逻辑是:
1.当 text[i]==pattern[j] 时, i++,j++
2.当 text[i]!=pattern[j] 时, j=next[j-1]
3.当 j==len_pattern 时, 结束查找
注意:因为要防止next数组越界,所以 j-1 >= 0 ,所以 j>0
所以第二条应该改为 当 j>0&&text[i]!=pattern[j] 时,j=next[j-1]
所以我们不妨构建一下这部分代码:
int My_strstr(char * text,char * pattern)
{
int len_pattern = strlen(pattern);
int len_text = strlen(text);
int j,i;
int *next=Getnext(pattern);
for(i=0;i<len_text;i++)
{
while(j>0&&text[i]!=pattern[j])
{
j=next[j-1]; //匹配失败移动j
}
if(text[i]==patterm[j])
{
j++; //单个元素匹配成功
}
if(j==len_pattern)
{
return i-len_pattern+1; //返回第一次成功匹配,text数组对应首元素的下标
}
}
return -1; //返回-1,意味着字符串不匹配
}
三·next数组的构建
关于next数组的构建实际上就需要用到前文提到的前缀与后缀
我们再来看一个视频:
大黄蜂录屏2023-08-18-15-52-50
从中我们不难发现几条规则:
1.当 pattern[i]==pattern[j] 时, i++;j++
2.当 pattern[i]!=pattern[j] 时, j=next[j-1]
3.当 i==len_pattern 时, 检索结束
注意:为了避免指针越界,j - 1>=0 所以 j > 0,所以第二条应该改为
当 j>0&&pattern[j]==pattern[i] 时,j=next[j-1]
所以我们依此来构建代码:
int *Getnext(char *pattern,int len_pattern)
{
int i,j;
int *next=(int *)malloc(sizeof(int)*len_pattern);
assert(next);
next[0]=0; //我们提前把,next第一个元素置为零,
//我们在动画中也演示了,第一个元素的前后缀均为空串
//其次,这样做可以让我们复刻 strStr()的逻辑写代码
for(i=1,j=0;i<len_pattern;i++) //更加容易记忆
{
while(j>0&&pattern[j]!=pattern[i])
{
j=next[j-1]; //内容不匹配时,回溯j
}
if(pattern[j]==pattern[i])
{
j++; //更新j,i随着循环自行更新,这里便不再,需要更新i
}
next[i]=j; //及时更新next数组
}
return next; //返回构建的next数组,一定要是动态开辟的数组
//一方面,next数组大小未知,其次,保证数组在出函
//数后不被销毁
}
四·关于kmp算法的再介绍
经过next数组构建后,你可能会心生疑惑:尽管代码我能看懂,但为什么可以这样做?
当你有这个疑问的时候,我便不能再给你解答了,这个时候,你应该去自己画画图,自己敲一敲代码,来熟悉里面的逻辑。(一下的解释便全是基于代码了)
如果你的逻辑还没有很清晰,不妨往下看:
首先你要清楚,构建next数组,包括使用next数组匹配字符串的过程中,“j++”这一条件,不是随着循环进行而不断执行的,他的增加是有条件的,这也其实就造成了,“j”比“i”慢的这种现象,再往深处刨析,j一定是在有对称的部分里存在的,他不可能位于一个非对称区域中(这一区域可能是整个字符串,但也可能不是。比如:abcde,没有对称区域,故j只可能在a元素对应的位置处。再比如:abcabcd,有对称区域abcabc,故j只可能存在于前一个abc对应的位置处)。关于这一点你可以去看看前面的视频演示,当然,这也只是基于代码的一种解释。
而对于i来说,它是一往无前的,是要遍历数组必须要进行的,所以将其设置成随循环递增。
而对于一些其他情况,比如说元素匹配时、不匹配时以及结束条件的判断这里就不多bibi了.
总结(内含一些推荐的KMP算法视频)
KMP算法的学习中,一定要多画图分析,分析结束后,一定要结合自己的分析,进行代码的编写与练习。
给大家推荐几个讲解KMP易懂的b站up主