kmp算法解决字符串匹配问题
思想核心:利用匹配失败后的信息,尽量减少模式串与主串的匹配次数
前缀:包含首字母、不包含尾字母的所有子串
如aabaaf的前缀有:
a、aa、aab、aaba、aabaa(aabaaf不是前缀)
后缀:包含尾字母,不包含首字母的所有字串
如aabaaf的后缀有:
f、af、aaf、baaf、abaaf(aabaaf不是后缀)
从首字符开始,逐个判断子串的最长相等前后缀,存入数组,即为前缀表,如aabaaf的前缀表:
a:0(a既没有前缀也没有后缀)
aa:1
aab:0
aaba:1
aabaa:2
aabaaf:0
此时的前缀表:
子串: a a b a a f
前缀表:0 1 0 1 2 0
以 主串aabaabaaf 与 模式串aabaaf 的匹配过程为例:
第一步——计算前缀表
如上文过程,计算得到模式串的前缀表
索引: 0 1 2 3 4 5
模式串: a a b a a f
前缀表: 0 1 0 1 2 0
匹配开始:
第二步——不断匹配直到发生不匹配或匹配完成:
主串: a a b a a b a a f
模式串: a a b a a f // 在f处发生冲突
第三步——发生冲突时,根据前缀表跳转到下一个进行匹配的位置
f处发生不匹配,查询其前一个字母的前缀表,查询结果为2,则跳到索引为2的位置继续进行匹配(移到最长相等前缀的下一位置,其索引刚好等于前缀表的值)
主串: a a b a a b a a f
模式串: a a b a a f // f处发生冲突
^
移动模式串:
主串: a a b a a b a a f
模式串: a a b a a f
^
// 将前缀移到后缀位置(前后缀相等,不需要再判断是否匹配)
// 从‘b’处重新开始匹配(最长相等前缀的下一位置,其索引值为2)
第四步——不断循环第二第三步直到匹配结束
遇到不匹配时,next数组告诉我们该回退到哪个位置(即前缀表的具体实现)
next数组的几种实现方式:
前缀表: 0 1 0 1 2 0
模式串: a a b a a f
next数组: 0 1 0 1 2 0
// 与前缀表相同,查询冲突元素的前一个元素
前缀表: 0 1 0 1 2 0
模式串: a a b a a f
next数组: -1 0 1 0 1 2
// 前缀表整体右移一位,首字母为-1,直接查询冲突元素
前缀表: 0 1 0 1 2 0
模式串: a a b a a f
next数组: -1 0 -1 0 1 -1 // 前缀表整体-1
相当于模式串自己与自己匹配(后缀与前缀匹配),通过递推的方式求出next数组
实现——双指针法(基于next数组的第一种实现方式)
i:遍历子串(不断向后移,不发生回退)
j:寻找该i值下的最长相等前后缀(冲突时利用next数组不断回退寻找最长相等前后缀)
一、初始化
i:指向后缀末尾,初始化为1(i != j,且首字母不需要判断)
j:指向前缀末尾,初始化为0(前缀末尾也代表着此时最长相等前后缀的长度)
next[0]初始化为0(首字母不需要判断,从第二个字符开始判断)
二、处理前后缀不相同的情况
j回退到next[j-1](遇到冲突看前一个元素的前缀表)
回退条件:j > 0(j = 0说明已经到了首字母,无法继续回退了)且s[i] != s[j](发生冲突)
// 如果发生冲突且j还能回退的话则继续回退
// 如果j回退到首字母,且首字母也冲突时退出回退循环
while( j > 0 && s[i] != s[j])
j = next[j - 1];
三、处理前后缀相同的情况
// 匹配成功则j后移,此时j之前的子串即为最长相等前缀
// j的值即为该最长相等前缀的长度
j++;
next[i] = j; // 更新i处的next数组值
i++; // 前移i
四、更新next数组的值
在前后缀相等情况时完成
KMP算法的代码实现
分为两个部分:
1、求模式串的next数组
2、使用next数组进行模式串与主串的匹配
vector getNext(string s) {
// 主要注意next[0]初始化为0(第一个字符不存在前后缀,next一定为0)
vector next(s.size(), 0);
// j指向前缀末尾,初始化为0(从首个字符开始判断)
int j = 0;
// i指向后缀末尾,初始化为1(第一个字符不需要判断,从第二个字符开始比较子串的前后缀)
for (int i = 1; i < s.size(); ++i) {
// 处理前后缀不相同的情况
// 如果发生冲突且j还能回退的话则继续回退
// 如果j已经回退到首字母,且首字母也冲突时退出回退循环
while (j > 0 && s[i] != s[j]) {
j = next[j-1];
}
// 处理前后缀相同的情况
// 匹配成功则j后移,此时j之前的子串即为最长相等前缀,将其赋值给next[i]
// ++i(进入下一次循环自动实现)
if (s[i] == s[j]) {
next[i] = ++j;
}
}
return next;
}
// 思路与获取next数组差不多(获取next数组的过程相当于模式串的后缀与前缀匹配)
int strStr(string haystack, string needle) {
vector next = getNext(needle);
int j = 0;
for (int i = 0; i < haystack.size(); ++i) {
// 发生冲突时j进行回退(这步的目的是利用匹配失败后的信息,减少下一次匹配的工作量)
while (j > 0 && haystack[i] != needle[j])
j = next[j - 1];
// 匹配成功时j后移
if (haystack[i] == needle[j])
++j;
// 如果此时j等于模式串长度说明找到了模式串在子串内的位置,进行返回
if (j == needle.size())
return i - needle.size() + 1;
}
return -1;
}
初看总觉得有点思路但又差一步,想了半天没想出来,看了视频解析才一下子恍然大悟。
KMP思路: 如果主串能够由重复子字符串组成,那么最长相等前后缀中不包含的那部分就是最小重复单元(需要一定数学推理)
实现:求主串长度与最长相等前后缀长度的差值,如果该差值能被主串长度整除则说明成立
bool repeatedSubstringPattern(string s) {
int n = s.size();
vector next(n, 0);
int j = 0;
// 求next数组,最后一个元素的next值就是最长相等前后缀长度
for (int i = 1; i < n; ++i) {
while (j > 0 && s[i] != s[j])
j = next[j - 1];
if (s[i] == s[j])
next[i] = ++j;
}
int ans = n - next[n - 1];
// 两个条件
return ans < n && n % ans == 0;
}