此篇博客将KMP算法在字符串匹配中的应用。
例:文本串:aabaabaaf
模式串:aabaaf
1️⃣ 理论篇
正常的匹配思路,对文本串的每一个位置,往右遍历模式串长度,若匹配则返回,不匹配则继续文本串下一个位置,复杂度 O ( m n ) O(mn) O(mn)
KMP算法:设置双指针,分别指向文本串和模式串的其中一个位置,文本串指针永不回溯,当不匹配时回溯模式串指针,复杂度 O ( n ) O(n) O(n)
在讲解KMP算法前,先介绍什么是前缀和后缀,对于字符串aab,前缀:a,aa,后缀:b,ab
我们定义这样一个结构:next数组,用来记录一个字符串中最长相等前后缀的长度
这个定义较为抽象,我们来介绍例子,对于串a,只有一个字符,定义为0;对于串aa,值为1;对于串aab,没有相等的前后缀,为0;对于aabaa,值为2.
根据上述工作,我们可以求出模式串每个前缀的最长相等前后缀长度,并得到next数组这么一个结构。
在实际匹配中,到第6个字符我们会发现不匹配,此时看模式串前缀aabaa,得到前缀数组中的值为2,之后便可让模式串指针从第6变成第3,继续匹配,可以有效减少回溯次数。
2️⃣ 代码篇
先说下next数组的变种,对于上面的模式串,next数组应该为0,1,0,1,2,0
第一个变种会右移一位(最后一个用不到),第一个变成-1,这样遇见冲突时不用比较上一位,直接查找冲突为的next值即可。
第二个变成会让每个数-1,这种不太常见。
本次代码采取next数组第一个变种。
接下来我们来讲解如何在
O
(
N
)
O(N)
O(N)时间复杂度内求next数组
正常的做法,即我们的做法肯定是对每个前缀,遍历得到值,但这样是
O
(
N
2
)
O(N^2)
O(N2)的时间复杂度。
设置i,j指针分别代表前缀的最后一个字符位置和后缀最后一个字符位置,同时i也是next数组的最后一个元素(可以思考一下为什么)
开始时,前缀不存在,所以i = -1,初始化next数组第一个元素是-1,j指向第一个元素。
由于i指向-1,所以无法匹配,0加入next数组,并让i等于0,j指向下一个元素。
此时就开始匹配了,如果i和j对应出的字符相同,说明可以由上一个i+1得到j处的next值。
如果不相同,就令i = next[i],这一步是最难思考的,i是下标i之前的最长相等前后缀长度,next[i]也是下标next[i]之前的最长相等前后缀长度。这两个是怎么结合在一起的呢?我们可以从next[i]的定义出发,从i到next[i]可以保证前后缀仍然有相等元素,只是长度会变短。理解了这个就懂了。
上面的理解还不够细,准确的说,如果不匹配,那么当前前后缀是相同的,找更短的最长相等前后缀找其中一个子串即可,一定是和找整个串等价。
而在匹配部分的代码和计算next数组是完全一致的,不同之处是前缀结尾i应该初始化为0,后缀结尾j也是0.
此处代码是leetcode 28题代码
class Solution {
public:
int strStr(string haystack, string needle) {
int n = needle.size();
if(n == 0)return 0;
int next[n];
memset(next, -1, sizeof(next));
int i = -1, j = 0; // i和j代表前缀最后位置和后缀最后位置
while(j<n-1)
{
if(i < 0 || needle[j] == needle[i])
{
i++;
j++;
next[j] = i;
}
else i = next[i];
}
// 根据next数据进行匹配
i = 0, j = 0;
while(j<haystack.size())
{
if(i<0 || haystack[j] == needle[i])
{
i++;
j++;
if(i == n)return j-n;
}
else i = next[i];
}
return -1;
}
};