目录
kmp算法的介绍
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)
——来自百度
kmp算法的前置思路
要从一个字符串(主串)中检查是否含有另一个字符串(模式串)并返回他的位置,首先想到的是暴力匹配,也就是让模式串从第一个字符开始,依次和主串的每一个字符比对,不一样就往右挪,一样就接着比对模式串第二个字符。当我们真的拿着两个物件(就比如一长一短两把尺子吧)这样比对了,会发现一步一步往右挪是不太聪明的。因为明明通常可以跳好几步,为什么每次只挪一步呢?那应该挪几步呢?
主串值 | a | b | a | b | a | b | c | d | c | d |
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
子串值 | a | b | a | b | c | d |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
上图在模式串下标4处不匹配了,于是模拟一下这个动作,就想象我们手里拿着模式串,往主串上靠,现在比对出下标4处的内容不一致了,我们手里拿着的模式串应该重新往右放到什么位置呢?首先,我们能感觉到如果直接拿着模式串的最左端对齐主串下标4处,是有可能错过一些情况的,而由于主串中红色部分一定不如模式串长,所以模式串最终还是要在4这个位置往右延申的,所以我们把模式串下标0对齐主串4,再从4这里往左挪一挪模式串,边挪边让模式串尽可能长的前缀和主串4左面匹配。
上述这一个手法就是kmp算法,有没有发现,我们生来就是算法人,只不过没有去总结,没有抽象出新概念,只是付出了行动而已。但只会行动不总结规律,我们就不能举一反三,总结出规律,抽象出新概念,这个概念打开我们的新视野,就像一双眼睛。
kmp算法的眼睛
接着上一步,从4这里往左挪一挪模式串。上面绿色字体是什么?是模式串上一步匹配成功的那一部分子串的前缀,我暂且把模式串的这个子串"abab"用 temp 来表示吧,绿色字体是 temp 的前缀;上面黄色字体是啥子?是上一次匹配成功的主串部分的后缀。由于 temp 和主串这一部分是完全匹配成功的,完全一样,所以黄色字体是temp的后缀呀!所以这是从4这里往左挪了多长?是"temp的最长公共前后缀"这么长。所以说模式串跟主串失配之后怎么调整位置?简单,因为下标是从0开始的,所以把模式串下标为“temp的最长公共前后缀长度”处的字符和当前失配点比对就ok。
“temp的最长公共前后缀长度”我愿称之为kmp算法的眼睛:"eye"。
kmp算法的next数组
我们用一个next数组存储模式串每一个下标处(不包括它)左侧 temp 的 eye 。为了方便描述 next 数组,我称这个 eye 为当前下标处的 eye 。比如本例子中"abab"的最长公共前后缀长度为模式串中 4 处的 eye。这样每当模式串在 j 处失配了,就如上述红字所说, j = next[j];
int Kmp(string& s, string& t, vector<int>& next) {
int i = 0, j = 0;
GetNext(t, next);
while (i < s.size() && j < (int)t.size()) {
if (j == -1 || s[i] == t[j]) {
++i; ++j;
}
else j = next[j];
}
if (j == t.size()) return i - j;
return -1;
}
上面 j == -1 是怎么回事 ? 能有这个疑问说明您没考虑周密,考虑周密的都卡在上一步红字那句话上了 (´ー∀ー`) 。1 处的eye是多少?他左面只有一个字符,一个字符最长公共前后缀长度是0还是1?这个我们不能定义,得让失配之后的重配操作去定义。模式串下标 j 在 1 处失配,把 j 重新赋为几? 赋为 0 啊,所以单个字符的最长公共前后缀的长度是 0 ,不光单个字符不太好理解,重叠的字符都是这个道理,简言之在kmp算法中最长公共前后缀不能取到 temp 的全部,最长就是 temp 的长度减 1。那么 0 处的 eye 是多少?一样看重配操作,当 j 在0处就直接失配,按照我们当前搭建起来的程序轮廓,j 会跑到它的 eye 处继续和主串当前位置比对,这个时候其实 j 这个位置已经没有意义了,所以 if 语句必须走进去,不能再走else了,在 if 语句中,我们需要通过旧操作实现新功能,显然当 j = 0 处失配,我们想让 j = 0 的模式串字符直接和主串下一个字符去比对,根据里面 ++i 和 ++j 的代码设计,我们把 next[0] 赋为 -1。当然,可以别这么卷,可以多写几个if,把 0 处的情况揪出来单独讨论,但是 kmp 算法就是牛的没办法。
实现GetNext函数,当然是在函数体内为next数组填充元素。那么模式串下标 0 处我们填充 -1,在下标 1 处填充 0, 我们用两个指针用head指向下标0处,用tail指向下标1处,把tail依次自增到模式串的倒数第二个字符处,每次比对head处的字符和tail处的字符是否相等,用head的自增和回溯操作来得到tail + 1 处的 eye 。这样就得到了模式串所有下标处的 eye 把它们存到了next数组里。好的,head怎么回溯 ?一个问题不说第二遍夯,head = next[head]。但是kmp算法是大牛算法,咱假装不知道下标 1 处填充0, 咱把head初始化为 -1, tail 初始化为 0。不说了,就是秀。
void GetNext(string& t, vector<int>& next) {
int head = -1, tail = 0;
next[0] = -1;
while (tail < next.size() - 1) {
if (head == -1 || t[head] == t[tail]) {
next[++tail] = ++head;
}
else {
head = next[head];
}
}
}
kmp算法中的归纳和构造两种思想
上一步里紫色文字,逗号左面是归纳,右面是构造。这个思想很牛啊,观察归纳代码设计中变量值的变化,构造出这些变量的预设值,这一步是叫预赋值还是啥的来着?这个是++j,要是j += 2就为next[0]预赋值为-2。以此类推。有了这个思想之后,应该能看出来head 初始化为 -1是怎么秀的了哈。next[++tail]有赋值为0的需求的时候head 应取 -1。
kmp算法完整代码实现
#include<iostream>
#include<string>
#include<vector>
using namespace std;
void GetNext(string& t, vector<int>& next) {
int head = -1, tail = 0;
next[0] = -1;
while (tail < next.size() - 1) {
if (head == -1 || t[head] == t[tail]) {
next[++tail] = ++head;
}
else {
head = next[head];
}
}
}
int Kmp(string& s, string& t, vector<int>& next) {
int i = 0, j = 0;
GetNext(t, next);
while (i < s.size() && j < (int)t.size()) {
if (j == -1 || s[i] == t[j]) {
++i; ++j;
}
else j = next[j];
}
if (j == t.size()) return i - j;
return -1;
}
int main() {
string s = "aaabcdefgaabcdac";
string t = "abcda";
vector<int> next(t.size());
cout << Kmp(s, t, next) << endl;
return 0;
}
next数组的小优化
就是考虑到当模式串在 j 处和主串失配了,j 回溯到 next[j]处如果还是同一个字符,那么必然继续失配,然后再回溯到next[next[j]]处,可是万一还是同一个字符呢?你想到了什么解决方法?没错,并查集的路径压缩,我们通过及时的压缩,把两层以上的集合都扼杀在摇篮里,就是下面的内层 if 语句。
void GetDeepNext(string& t, vector<int>& next) {
int head = -1, tail = 0;
next[0] = -1;
while (tail < next.size() - 1) {
if (head == -1 || t[head] == t[tail]) {
++head; ++tail;
if (t[tail] == t[head]) next[tail] = next[head];
else next[tail] = head;
}
else head = next[head];
}
}
最后,有没有大佬教一下怎么才能既能先写标题有能让csdn的目录点击跳转正确?我先写的标题,然后往里面插入文字就把标题的位置影响了。