解决问题:
KMP算法主要解决的问题就是在字符串(主串)中的模式(pattern)定位问题。记主串为T,模式串为P,则KMP算法就是返回P在T中出现的具体位置,如果没有出现则返回-1。
如图所示,对于这一问题,最简单的思路就是:从左至右一个个匹配,如果在这个过程中有某个字符不匹配,则从当前主串匹配的起始位的下一位开始重新匹配。具体过程如下图所示。
初始化:
如果 i 指针指向的字符和 j 指针指向的字符不一致,那么把 i 右移1位,j 从0位开始,从新开始匹配:
如果 i 指针指向的字符和 j 指针指向的字符一致,则 i 与 j 都向后移动
基于以上想法得到了以下程序:
/*
暴力破解法
ts 主串
ps 模式串
如果找到,返回在主串中第一个字符出现的下标,否则为-1
*/
class Solution{
public:
int bf(string t, string p) {
int i = 0; // 主串的位置
int j = 0; // 模式串的位置
while(i < t.length() && j < p.length()){
if (t[i] == p[j]) { // 当两个字符相同,就比较下一个
i++;
j++;
}else{
i = i - j + 1; // 一旦不匹配,i后退
j = 0; // j归0
}
}
if(j == p.length()){
return i - j;
}else{
return -1;
}
}
};
暴力法能够解决模式串匹配的问题,但不够好。主要的问题在于匹配失败之后我们对主串进行了回退(即:i = i - j + 1)。以下图为例:
此时 C 与 B 不匹配,如果按照暴力破解的方法,我们会把i置于第1位,但如果我们要保持 i 不回退,我们应该怎么做。KMP算法的思路是:“利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置。”
在这通过观察可知合理做法是把j移动到2位。为什么?
直观的说是因为前面存在 A B 相同。而根本的原因在与:由于不匹配的是 C 与 B,这说明在这之前P串与T串是匹配的(均为:A B C A B),而对于 A B C A B 这一部分存在着前缀(A B)与后缀(A B)相等,因此在T串与P串中存在着相同的前缀与后缀。故P串的前缀与T串的后缀相同,所以我们将j的位置调整至相等前缀的后一位。
用数学表达就是:
当:T[i] != P[i] 时,有T{ [i - j] ~ [i - 1] } = P{[0] ~ [j - 1]}
同时如有P{ [0] ~ [k - 1] } = P{ [j - k] ~ [j - 1] }
必有:T{ [i-k] ~ [i-1] } = P{ [0] ~ [k - 1] }
结合下面图片理解:
上述的存在相等的前缀与后缀解释了:调整 j 的位置后 i ,j 之前字符串仍然匹配的原因,下面介绍不修改 i 位置的正确性。以下图为例:
首先,不修改 i 的位置相当于去除了以 i = 1 (i - j+1) ~4 (i - 1) 为起始位置的所有情况,那么证明不修改 i 位置的正确性可以等价与证明从以(i - j+1)~(i - 1)开头不存在可能解。可能解即P串与T串匹配。对模式串P的{ [0] ~ [j - 1] }分析,如果存在某一前缀与后缀相同则必然从T中后缀开始处匹配P的前缀才能一直匹配至原不匹配处。
从上述的分析可知想让 i 不后退,需要知道 j 调整到位置,而 j 调整的位置取决于P{ [0] ~ [j - 1] } 中最长前缀与后缀相等的位置,如有P{ [0] ~ [k - 1] } = P{ [j - k] ~ [j - 1] },那么 j 就调整到 k 位。我们用数组保存这一跳转信息,称之为next数组。
next数组定义为:匹配模式串j位置失配,j应调整到的新位置。根据 j 的调整方法与模式串前 j-1 位最长前后缀相等的关系,可知next[j]的值应为模式串前 j - 1 位最长前后缀相等的前缀末尾下一位。
下面给出next数组求法和解释:
class Solution{
public:
vector<int> next;
vector<int> getNext(string p){
int n = p.length();
next.resize(n);
next[0] = -1;
int j = 0;//后缀待判断位
int k = -1;//前缀待判断位
while(j < n - 1){
if(k == -1 || p[j] == p[k]){
next[++j] = ++k;
}else {
k = next[k];
}
}
return next;
}
};
首先,重述一遍next[j]的含义:当P[j] != T[i]时,j指针的下一步移动位置。同时也是对模式串P前 j - 1 位最长前后缀相等时前缀末尾的下一位
初始化:当 j 为0时,如果这时候不匹配,怎么办?
j已经不能再左移了,也就是i在这一位已经不可能与模式串匹配了,这时候说明 i 可以直接右移一位。为了与P[j] =T[i]情况一起处理(i++,j++),定义next[0] = -1,得 i 指向下一位,j 为0。
当 j 为1时,如果这时候不匹配?
显然,j指针一定后移到0的位置。
再重述一遍next[j]的含义:当P[j] != T[i]时,j指针的下一步移动位置。同时也是对模式串P前 j - 1 位最长前后缀相等时前缀末尾的下一位
一般情况,由于我们的next数组是按顺序初始化的,假设我们现在在处理前缀位k与后缀位i,求next[j + 1],如下图所示:
因为比对到了 k 与 j ,说明P{ [0] ~ [k - 1] } 与 P{ [j - k] ~ [j - 1] }匹配(next[j] = k)
如果P[k] == P[j],那么P{ [0] ~ [k] } 与 P{ [j - k] ~ [j] }匹配,那么next[j+1] = k + 1=next[j] + 1(因为next[j+1]表示在前j位中最长前后缀相等的前缀末尾的下一位也就是k+1)。
如果P[k] != P[j],说明此时前j位的最长前缀到不了第 k 位,为了求next[j+1],我们要继续寻找P[?] = P[j],
图示A1与A2是相等的前后缀,由于P[k] != P[j]所以我们继续寻找前 j 位的最长前后缀。我们取出next[k]所在的位置,由next数组的定义,B1与B2相等,而由A1与A2相等,故存在B3与B2相等=>B1与B3相等,那么我们就可以继续判断P[j] 是否等于 P[next[k]],如果相等就可以得到next[j + 1],不等继续递归,直到k=-1,执行i++,k++。
好了,有了next数组之后我们就可以写KMP算法了:
class Solution{
public:
vector<int> next;
vector<int> getNext(string p){
int n = p.length();
next.resize(n);
next[0] = -1;
int j = 0;
int k = -1;
while(j < n - 1){
if(k == -1 || p[j] == p[k]){
next[++j] = ++k;
}else {
k = next[k];
}
}
return next;
}
int KMP(string t, string p){
int i = 0; // 主串的位置
int j = 0; // 模式串的位置
next.clear();
getNext(p);
while(i < t.length() && j < p.length()){
if(j == -1 || t[i] == p[j]){
i++;
j++;
}else{
j = next[j]; // j调整到合适位置
}
}
if(j == p.length()){
return i - j;
}else{
return -1;
}
}
};
最后引用WNJXYK大佬的模板:
build为构建next数组,改进的地方为把所有匹配解存入容器中返回
具体讲解可通过链接查看
namespace KMP{
vector<int> next;
void build(const string &pattern){
int n = pattern.length();
next.resize(n + 1);
for (int i = 0, j = next[0] = -1; i < n; next[++i] = ++j){
while(~j && pattern[j] != pattern[i]) j = next[j];
}
}
vector<int> match(const string &pattern, const string &text){
vector<int> res;
int n = pattern.length(), m = text.length();
build(pattern);
for (int i = 0, j = 0; i < m; ++i){
while(j > 0 && text[i] != pattern[j]) j = next[j];
if (text[i] == pattern[j]) ++j;
if (j == n) res.push_back(i - n + 1), j = next[j];
}
return res;
}
};
参考:
详解KMP算法
KMP算法的Next数组详解
如果next数组分析还是不清晰可以看看参考里唐小喵的next数组详解