KMP算法是一种在给定文本串T中查找是否包含特定模式子串P的一种较高效的算法。
BM算法
一般地,我们会想到用两个指针:ti指向文本串T的头部,pi指向模式串P的头部;如T[ti] == P[pi],则ti , pi 同时向右移动;否则pi回撤到0,ti回撤到T、P对齐位置的后一位;最终如果pi到达P的末尾,则T中存在子串P;如果ti到达T的末尾而pi未到达P的末尾,则T中不存在子串P。这种暴力查找算法被称作BM算法。
KMP算法
KMP算法和BM算法的主体骨架是一样的,都是两个指针比较并向后移动;区别在于当两个指针下标的元素不相同时,BM算法ti和pi都需要回撤,而KMP算法中,ti是不回撤的,pi也不需要回撤到0,而是根据一个next数组,确定要回撤到哪个位置。
next数组
next数组的作用是,当 T[ti] != P[pi] 时确定pi应该回撤到哪个位置。规定 next[0] = -1,next[1] = 0 。 next[pi]的值为P[0:pi-1]的最长公共真前、后缀(注意:next[pi]对应的是P[0:pi-1]的真前后缀,也就是不包括P[pi])的长度(pi>0)。
next数组的求法
- 首先 next[0] = -1, next[1] = 0
- 对于next[i] = 2,...,lenP-1, 令j指向前缀字符串末尾后一位,初始化时没有前缀字符串,其值为0。
- 当P[i-1] == P[j],说明P[0:j]和P[i-1-j:i-1]是公共最长真前后缀,故而next[i] = j+1;
- 当P[i-1] != P[j], 则j 需要回撤,回撤到next[j] 如果P[i-1]还是不等于P[j]且j != 0 ,则j继续回撤(为什么需要回撤:回撤的过程和实际匹配的过程是一样的,实际匹配时也是不断对pi进行回撤)
最终实现
void bulidNextArray(const string& p, vector<int>& next) {
const int N = p.size();
next.resize(N);
next[0] = -1;
if(N == 1)
return;
next[1] = 0;
for(int i = 2, j = 0; i < N; i++) {
while(j != 0 && p[i-1] != p[j]) {
j = next[j];
}
if(p[i-1] == p[j]) {
j++;
}
next[i] = j;
}
}
int KMP(const string& t, const string& p) {
const int M = t.size(), N = p.size();
if(M * N == 0) {
return -1;
}
vector<int> next(N);
bulidNextArray(p, next);
int ti = 0, pi = 0;
while(ti != M && pi != N) {
if(t[ti] == p[pi]) {
ti++;
pi++;
} else {
pi = next[pi];
if(pi == -1) {
pi = 0;
ti++;
}
}
}
if(ti == M && pi != N) {
return -1;
} else {
return ti - N;
}
}