目的
本文不是为了详解KMP算法的整个流程,而只是教大家如何理解求解前缀表的过程。如果前缀表能顺利写出来,那么字符串匹配就十分好理解了。
一、背景
先简单补充一点名词解释:
- KMP算法:求解
子串在父串中是否出现以及第一次出现的位置
的算法。 - 前缀:
以第一个字符开头、不包含最后一个字符的所有连续子串
均称为前缀; - 后缀:
以最后一个字符结尾、不包含第一个字符的所有连续子串
均称为后缀; - 最长相等(公共)前后缀:
前缀子串 = 后缀子串
且长度最长
; - 前缀表:记录
下标i之前(包括i)的字符串的最长相等前后缀的长度
。
二、求解前缀表
1. 关键
我觉得很重要的一点,也是很多文章都没有说明白的一点,就是:假设str[0, i] 是我们要求最长相等前后缀的字符串,那么,它的最长相等前后缀的长度是与str[0, i - 1]密切相关的!
正是这种依赖关系,导致我们在查找str[0, i]的最长相等前后缀的长度时,只需比较str[i] 与 str[ vNext[i - 1] ] 的关系即可得出结论
。
这里面,
vNext[i - 1] 表示 str[0, i - 1] 的最长相等前后缀的长度;
str[ vNext[i - 1] ] 表示 str[0, i -1] 的最长相等前缀的后一个字符,即第一个不匹配字符。
所以,求解前缀表的过程,就是在比较新加入的字符str[i] 与 第一个不匹配字符str[ vNext[i - 1] ] 是否相等?如果不相等,则下一个不匹配字符缩短最长相等前后缀的长度,再次比较缩短后的最长相等前后缀的第一个不匹配字符。
2. 模拟
对于字符串 aabaac
,推理过程如下:
1)当 i = 0 时,str[0,i] = a
,没有前后缀,则最长相等前后缀为0,则 vNext[0] = 0
,令 k = vNext[0] = 0 来记录 str[0, i - 1]的最长相等前后缀的长度
;
2)当 i = 1 时,str[0,i] = aa
,此时比较新加入的字符str[i] = str[1] = a
与上一个子串的第一个不匹配字符 str[k] = str[0] = a
, 两者相等,则str[0, i] 的最长相等前后缀 比 str[0, i -1]的最长相等前后缀 多 1
,则 vNext[i] = k + 1 = 1
, 同时更新 k = vNext[i] = 1
;
3) 当 i = 2 时,str[0, 2] = aab
,此时比较新加入的字符str[i] = str[2] = b
与上一个子串的第一个不匹配字符 str[k] = str[1] = a
, 两者不相等,说明:
– a) str[0, i] 的最长相等前后缀的长度比str[0, i -1] 的短
;
– b) str[0, i] 的最长相等前后缀只可能出现在str[0, i -1] 的最长相等前后缀里面。str[0, i -1] 的最长相等前后缀 = str[0, vNext[k - 1]]。这里 k - 1 是因为假设长度为2,则 index= 2 - 1 = 1
。
综上两点,k = vNext[k - 1] = vNext[1 - 1] = 0
;
在 k = 0 之后,str[i] = str[2] = b, str[k] = a 两者仍不相等。但因为此时的最长相等前后缀已经为0,没有可以再减少的了,所以vNext[i] = vNext[2] = 0;
3. 代码
void GetNext(const string& str, vector<int>& vNext)
{
vNext.resize(str.size());
vNext[0] = 0;
int i = 0; // str[0 ~ i]表示当前正在求的子串
int k = vNext[0]; // k表示的是str[0, i - 1]子串的最长相等前后缀的长度,所以str[k] 表示的是str[0, i - 1]子串最长相等前后缀的前缀的后一个字符
while(i < str.size())
{
if(str[i] == str[k]) // 说明str[0, i]的最后一个元素与str[0, i - 1] 的第一个不相等的元素相等
{
k++; // 则最长相等前后缀的长度加1
vNext[i] = k; // 也即是当前str[0, i]的最长相等前后缀长度
i++; // 求解下一个子串
}
else // 如果不相等,说明之前的最长相等前后缀需要缩短
{
if(k == 0) // 退无可退,则等于0
{
vNext[i] = 0;
++i;
}
else
{
k = vNext[k - 1];
}
}
}
}