KMP算法
KMP
最近做其他题的时候用到了KMP的算法思想,所以再写下KMP算法的笔记增加印象。
KMP算法用于匹配字符串,比起暴力优化了时间复杂度。
思路
KMP的精髓是next
数组。
模式串(短的那个)与主串(长的那个)匹配时,若失配(p[j] != s[i]
)时,在主串指针不动的情况(在主串指针遍历完主串之后就可以获得整个匹配的记录)下,仅向后移动模式串指针就能够继续进行匹配,next
数组就记录了模式串指针应该跳回的下标。
进一步分析,模式串指针应该跳回的下标是什么?要跳到什么程度才能继续进行匹配?(暴力法直接从头开始)
- 失配时,当前位是不匹配的,但是以前的所有位都是匹配成功的。
能不能利用这一点? - 模式串指针当前只能向模式串的前方移动(同时可视作模式串相对主串向后移),移到什么程度才能继续进行匹配?
- 首先要保证匹配成功的最后一位还是相同的,不然没法比啊?
- 同理,保证匹配的倒数第二位是相同的
- ...
- 一直到移动后的模式串的首位,依然要与移动前的这个位置还是相同的,如图
- 灰色线段前,模式串移动前后是自相匹配的。
- 由于给出的条件已经保证了两段模式串移动前后重叠的部分,所以接下来只需要试着匹配黑色部分能否匹配。
- 若黑色部分能继续匹配,那么继续匹配直到匹配结束即可。
- 若不能匹配,继续前移模式串,满足如上条件即可。
next
数组的作用就是这样。
暴力匹配的话是需要将主串指针也向后移的,最坏情况下O(n * m)
,因此增加了时间复杂度。
PS:其实KMP的定义也有种状态转换的意思,模式串指针的移动也代表了模式串指针所指向的这两个状态也是可以相互变换的。
代码
先附一下代码:
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n, m;
char p[N], s[N];
int nex[N];
int main()
{
cin >> n >> p + 1 >> m >> s + 1;
for (int i = 2, j = 0; i <= n; i++) {
while (j && p[i] != p[j + 1])
j = nex[j];
if (p[i] == p[j + 1])
j++;
nex[i] = j;
}
for (int i = 1, j = 0; i <= m; i++) {
while (j && p[j + 1] != s[i])
j = nex[j];
if (p[j + 1] == s[i])
j++;
if (j == n) {
cout << i - n << " ";
j = nex[j];
}
}
return 0;
}
按上面的思路,如何编写代码,完成计算next
数组和原主串匹配这两个任务呢?
计算next
数组
上面的思路不仅适用于计算模式串与主串,模式串与模式串自相匹配就可以得到next
数组
假设已经知道前i - 1
字符的next
数组数值(图中灰色线段前的部分),现目标是求出黑色线段所指向的第i
个字符的next
值。
灰色线段前的部分是相等的,对于黑色线段所指向的位置:
- 如果指向的部分仍是相等的,那直接就可以求出黑色线段的
next
值为:next[i] = next[i - 1] + 1
- 若不相等,继续后移下方的模式串,直到能再次匹配为止,这个过程即
j = i - 1, j = next[j]
。
至于循环条件,要么黑线位置处相等,要么移到头,即p[i] != p[j + 1] && j
退出循环有两种可能:p[i] == p[j + 1]
,则黑线部分是匹配的,next[i] = j + 1
j == 0
,则表表示搜到头也没有,即没有可利用的前后缀重复,next[i] = 0
还可以发现一些可以利用的性质,j
如果一开始赋0,它就一直代表上一个的next
数值(第一位的next
肯定是0),这样我们就可以写出求next
的代码
- 为了方便,数组从1开始
for (int i = 2, j = 0; i <= n; i++) {
while (j && p[i] != p[j + 1])
j = nex[j];
if (p[i] == p[j + 1])
j++;
nex[i] = j;
}
最后模式串和主串的匹配同理。