KMP算法,也就是我们常说的“烤馍片”,是字符串类型题里面很重要的算法之一,主要用于字符串匹配。当出现字符串不匹配时,使用KMP算法就可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。学习KMP,一方面是为了利用这个算法做题,另一方面也是通过对kmp的运用来更加深入的理解字符串这个数据类型的本质。
而KMP算法中的精髓就是前缀表,整个算法都是基于前缀表而存在的。
关于前缀表,你需要了解这些:
- 前缀和后缀是什么?最长相等前后缀是什么?
- 前缀表是什么?前缀表怎么算?
- 前缀表有什么用?前缀表怎么用?
以上问题我们来一一解答。
一、前缀&后缀&最长相等前后缀
给你一个字符串,aabaaf
,它的前缀和后缀都分别有五个,一一列举下来:
大家也许能看出来一些前缀和后缀的特点了。
前缀,就是字符串里不包含最后一个字符的、以第一个字符开头的所有子串;
后缀,就是字符串里不包含第一个字符的、以最后一个字符结尾的所有子串。
最长相等前后缀又是什么呢?我们还是以aabaaf
为例。我们在上面已经把它的所有前后缀列出来了,我们可以看出,它根本没有相等的前后缀,所以aabaaf
的最长相等前后缀的长度为0。
我们再以aabaa
为例,我们会发现,它有两对相等前后缀,分别是a和a,aa和aa。那么aabaa
的最长相等前后缀的长度就是2。
二、前缀表是什么&怎么算
前缀表的直接解释就是,在字符串里,长度为前n个字符的子串,它们最长相等前后缀的长度,所组成的数组。在题目里我们经常给它起名叫next数组。可能有点难理解,我们还是以aabaaf
为例,它的前缀表是[0,1,0,1,2,0]
。我们接下来演示一下它的前缀表是怎么算的,同时去理解前缀表的含义。
-
在字符串
aabaaf
中,长度为前1个字符的子串a
,它的最长相等前后缀的长度为0。与之对应的,next数组里的第1位成员的值就为0。
-
在字符串
aabaaf
中,长度为前2个字符的子串aa
,它的最长相等前后缀的长度为1。与之对应的,next数组里的第2位成员的值就为1。
-
在字符串
aabaaf
中,长度为前3个字符的子串aab
,它的最长相等前后缀的长度为0。与之对应的,next数组里的第3位成员的值就为0。
-
在字符串
aabaaf
中,长度为前4个字符的子串aaba
,它的最长相等前后缀的长度为1。与之对应的,next数组里的第4位成员的值就为1。
-
在字符串
aabaaf
中,长度为前5个字符的子串aabaa
,它的最长相等前后缀的长度为2。与之对应的,next数组里的第5位成员的值就为2。
-
在字符串
aabaaf
中,长度为前6个字符的子串aabaaf
,它的最长相等前后缀的长度为0。与之对应的,next数组里的第6位成员的值就为0。
好的,然后我们把next数组里六位成员值都写出来,就能得到前缀表[0,1,0,1,2,0]
了。
三、前缀表有什么用&怎么用
了解了前缀表是什么,那我们就要问前缀表有什么用了。前缀表的作用就是回退,意思是当我们在匹配字符串的时候,遇到了不匹配的字符,我们就可以通过查询前缀表,回退到上一个匹配过的地方继续匹配,而不用重头匹配,从而减少匹配的次数。那前缀表是怎么实现这个作用的?
我们举一个例子,假设我们要在文本串(s):aabaabaaf
中查找是否出现过一个模式串(l):aabaaf
。那么首先我们需要得到模式串的前缀表,[0,1,0,1,2,0]
。接着我们开始匹配这两个字符串。
前面的匹配都没有问题,到这里出问题了:
匹配不上了。我们就要使用前缀表来进行回退。我们读取当前模式串字符前一位的前缀表,也就是next[4]=2。将指针退至前缀表里的值,所对应的模式串下标的字符位置,也就是 l[2]=b。如图:
这样,我们就避免了再匹配一次aa的情况,节省了时间。这就是前缀表的作用和用法,也就是KMP的原理。
例题里对KMP的实际应用:
下面展示两道比较基础的匹配字符串的题目,旨在展示KMP的代码实现过程及思路。
(模版题)力扣28.找出字符串中第一个匹配项的下标
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
示例 1:
输入:haystack = “sadbutsad”, needle = “sad”
输出:0
解释:“sad” 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = “leetcode”, needle = “leeto”
输出:-1
解释:“leeto” 没有在 “leetcode” 中出现,所以返回 -1 。
提示:
1 <= haystack.length, needle.length <= 104
haystack
和needle
仅由小写英文字符组成
题目分析
使用KMP匹配字符串,haystack
是文本串,needle
是模式串,最后返回第一个匹配项的下标。
解题思路
我们现在来进行KMP的代码实现。为了更清晰直观,我们把求next数组的过程单拎出来写成函数的形式。求next数组我们可以分为四个步骤:
- 初始化
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
- 更新next
首先进行初始化。函数命名为getNext,传入函数的参数为一个next数组和模式串needle
。我们先用函数strlen
获得needle
的长度并赋值给m,定义前缀末尾j=0(j在后面用于匹配寻找最长相等前后缀),并把0赋值给next[0](因为前缀表的第一位肯定是0,单个字符没有相等前后缀)。然后设置for循环,用来移动后缀末尾i。这个时候我们的i就要从1开始遍历了,而不是从0。因为我们要比较needle[j]
和needle[i]
,也就是比较前缀末尾和后缀末尾,i从1开始才能比较。条件是i<m,最后执行i++。初始化结束。
接下来我们处理前后缀不相同的情况。判断条件肯定有needle[i]!=needle[j]
,判断了以后,对前后缀匹配不上的位置进行回退的操作。我们要读取当前j的前一位的next数组的值,也就是j=next[j-1]
。“遇见冲突看前一位”,这是KMP实现的一个不变量(如果是以这种格式写的next数组的话)。也因为存在这个不变量,我们的判断条件必须加上一个j>0。当数组中出现回退的操作,j-1,我们就不得不考虑它的越界访问问题,在判断的时候我们就要规避掉。
关于此处的判断是使用if还是while,也是容易出错的一个地方。我们回退的操作会进行几步?一定是一步吗?这个问题需要仔细地全面的思考。如果想不来,就举例子模拟。假设字符串aaaf
,我们遍历到i=3,j=2的时候,就会发现此时就匹配不上了。如果我们用if,就代表j只会回退一步。回退一步后的j=next[j-1]=next[1]=1,接着运行程序的话,就会直接把j的值赋给next[i],也就是赋给next[3],但是很明显next[3]应该等于0,而不是1。这就代表这个时候的j回退不到位,我们得到了错误的前缀表,题也就不可能做对了。所以此处的判断必须使用while。
接着我们处理前后缀相等的情况。相等的情况就使用if判断就行,不存在需要多次的情况。如果判断出来此时前后缀相等,我们就让j++。因为j作为字符串下标,会比正常的计数都要小上一个1,我们需要把它加回来才能得到正确的前缀表。
最后更新next,就直接把此时j的值赋给next[i]就可以了。至此,我们求next数组的函数就写完了,下面开始写主函数。
在主函数里,我们的操作几乎和上面的一致,只是把上面对前缀末尾j和后缀末尾i的匹配,变成了对文本串haystack和模式串needle的匹配,并且最后不需要更新next,只用做前三步即可。需要注意的就是,我们要对模式串needle
长度为0的情况进行特判,因为在这种情况下是肯定不需要匹配的,直接return 0。最后加一个if(j==m)
,判断是否匹配到了模式串的末尾,如果满足条件就返回i-m+1
,即第一个匹配项的下标。最后没有匹配到的情况,返回-1。
代码实现
void getNext(int* next,char* needle){
int m=strlen(needle);
next[0]=0;
int i=0,j=0;
for(i=1;i<m;i++){
while(needle[i]!=needle[j]&&j>0){
j=next[j-1];
}
if(needle[i]==needle[j]) j++;
next[i]=j;
}
}
int strStr(char* haystack, char* needle) {
int n=strlen(haystack);
int m=strlen(needle);
if (m == 0) return 0;
int* next = malloc(sizeof(int) * m);
getNext(next, needle);
int j=0;
for(int i=0;i<n;i++){
while(haystack[i]!=needle[j]&&j>0){
j=next[j-1];
}
if(haystack[i]==needle[j]) j++;
if(j==m) return (i-m+1);
}
return -1;
}
力扣1392.最长快乐前缀
「快乐前缀」 是在原字符串中既是 非空 前缀也是后缀(不包括原字符串自身)的字符串。
给你一个字符串 s
,请你返回它的 最长快乐前缀。如果不存在满足题意的前缀,则返回一个空字符串 ""
。
示例 1:
输入:s = “level”
输出:“l”
解释:不包括 s 自己,一共有 4 个前缀(“l”, “le”, “lev”, “leve”)和 4 个后缀(“l”, “el”, “vel”, “evel”)。最长的既是前缀也是后缀的字符串是 “l” 。
示例 2:
输入:s = “ababab”
输出:“abab”
解释:“abab” 是最长的既是前缀也是后缀的字符串。题目允许前后缀在原字符串中重叠。
提示:
1 <= s.length <= 105
s
只含有小写英文字母
题目分析
求next数组,得到next数组的最后一位,创建一个新字符串放答案。
解题思路
这道题如果你会KMP了就很好办,因为从题目的描述里完全就可以看出来,他这个所谓的最长快乐前缀就是最长相等前后缀。我们只需要得到next数组,并且我们只需要它的最后一位元素的值,也就是next[n-1]。因为最后一位元素对应的是原字符串的最长相等前后缀,我们不需要其他子串的最长相等前后缀。
接着依旧是上一道题求next数组的过程,没有任何变化,理解了就很容易写出来。
得到以后,定义一个max变量用来保存next[n-1]的值,再+1是为了预留添加终止符‘\0’的空间。我们创建一个新字符串,给它分配一块大小为max的内存。然后用for循环把字符串s的前缀录入字符串l里,最后添加终止符,输出l,这道题就做完了。
代码实现
char* longestPrefix(char* s) {
int i,j=0,max=0;
int n=strlen(s);
int*next=(int*)malloc(sizeof(int)*n);
next[0]=0;
for(i=1;i<n;i++){
while(j>0&&s[i]!=s[j]){
j=next[j-1];
}
if(s[i]==s[j]) j++;
next[i]=j;
}
max=next[n-1]+1;
char*l=(char*)malloc(sizeof(char)*max);
for(i=0;i<max;i++){
l[i]=s[i];
}
l[max-1]='\0';
return l;
}