目录
问题简单描述:
给定字符串s和模式串p,求模式串p在字符串s中出现的位置(所有位置都要给出),假设给定字符串s="ababcdabcabcd",模式串p="abcd"
朴素思路解法
暴力搜索:模式串逐个向右移动,复杂度O(M*N)
//字符串s
a b c a b c a b k
//模式串 a b c a b k
a b c a b c a b k
a b c a b k
a b c a b c a b k
a b c a b c a b k
a b c a b c a b k
a b c a b c a b k
a b c a b c a b k
............................
在这里,我们可以这样考虑根据已经扫描过的子串信息,直接将模式串右拉,例如当前s[i]与p[j]不想等,我们希望p[j]的前一个字符串p[j-1]形成的后缀和p[0]开始构成的前缀信息有重合,这样我们就可以直接将模式串拉到重合的位置,省略搜索时间。例如,在上诉例子中:当s[5]=c与p[5]=k不相等时,根据p[4]的最长公共前后缀(ab)直接将模式串abcabk拉到继续重合的位置,然后再进行后续判断。
//字符串s
a b c a b c a b k
//模式串 a b c a b k
a b c a b c a b k
a b c a b k
a b c a b k//匹配成功
............................
KMP算法
next数组的求解
根据朴素思路提到的子串的最长公共前后缀(不能是整个字符串)长度,也就是KMP算法中的next数组,下面给出一个next数组的例子。例如下面的aba,其前缀为a ab(aba不算在里面),其后缀为ba a(aba不算在里面),所以公共前后缀为a,长度为1。其他的依次类推。
回顾next数组的完整定义:
- 定义 “k-前缀” 为一个字符串的前 k 个字符; “k-后缀” 为一个字符串的后 k 个字符。k 必须小于字符串长度。
- next[x] 定义为: P[0]~P[x] 这一段字符串,使得k-前缀恰等于k-后缀的最大的k.
这个定义中,不知不觉地就包含了一个匹配——前缀和后缀相等。接下来,我们考虑采用递推的方式求出next数组。如果next[0], next[1], ... next[x-1]均已知,那么如何求出 next[x] 呢?
分情况讨论。首先,已经知道了 next[x-1](以下记为now),如果 P[x] 与 P[now] 一样,那最长相等前后缀的长度就可以扩展一位,很明显 next[x] = now + 1. 图示如下。
刚刚解决了 P[x] = P[now] 的情况。那如果 P[x] 与 P[now] 不一样,又该怎么办?
那我们现在需要寻找的是字符串[0.....x]的最长公共前后缀,我们可以知道:公共前缀肯定在子串A中,公共后缀肯定在子串B中,由于子串A和子串B是完全相等的,问题又转化成为求解子串A的最长公共前后缀即now=next[now-1],即如下图所示。然后再去判断p[now]与p[x]相等否重复上诉过程。
具体的代码实现可以参考如下:
void get_next(vector<int> &next, string p){
//根据给定的模式串求解next数组
//next[i]数组表示模式串[0---i]的最长公共前后缀长度
next.resize(p.length(), 0);
int j = 0;
next[0] = j;
for(int i = 1; i < p.length(); i++){
//j最开始进入循环时是next[i-1]的值,即[0----i-1]的最长公共前后缀长度
while(j > 0 && p[i] != p[j]){
/*s[i]与s[j]不相等,则必须进行回退
本来是要求A [0----j-1]与 B[*---i-1]的最长A前缀与B后缀的长度,由于A B相同
实质上是转化成为求A的最长前后缀长度
*/
j = next[j-1];
}
if(p[i] == p[j])
//在公共前后缀长度j上直接+1就行
j++;
next[i] = j;
}
}
模式匹配的过程
和next数组的构建过程也即为相似,代码如下所示:
void match(string s, string p, vector<int> next){
//字符串s和p的匹配代码
for(int i = 0, j = 0; i < s.length(); i++){
//i表示字符串s当前扫描到的位置,j表示模式串当前扫描到的位置
while(j > 0 && s[i] != p[j]){
//s[i]与p[i]不相等,直接将模式串p向后移动
/*
字符串s: * * a b c g k h f a b c d * * * * * * * * *
模式串P: a b c g k h f a b c g
a b c g k h f a b c g
a b c g k h f a b c g
a b c g k h f a b c g
对串s已经扫描的部分,我们应该
*/
j = next[j-1];
}
if(s[i] == p[j]){
j++;
}
if(j == p.length()){
cout << "match_end :" << i << endl;
j = 0;
}
}
}
完整代码如下所示:
#include "bits/stdc++.h"
using namespace std;
//KMP算法实现
void get_next(vector<int> &next, string p){
//根据给定的模式串求解next数组
//next[i]数组表示模式串[0---i]的最长公共前后缀长度
next.resize(p.length(), 0);
int j = 0;
next[0] = j;
for(int i = 1; i < p.length(); i++){
//j最开始进入循环时是next[i-1]的值,即[0----i-1]的最长公共前后缀长度
while(j > 0 && p[i] != p[j]){
/*s[i]与s[j]不相等,则必须进行回退
本来是要求A [0----j-1]与 B[*---i-1]的最长A前缀与B后缀的长度,由于A B相同
实质上是转化成为求A的最长前后缀长度
*/
j = next[j-1];
}
if(p[i] == p[j])
//在公共前后缀长度j上直接+1就行
j++;
next[i] = j;
}
}
void match(string s, string p, vector<int> next){
//字符串s和p的匹配代码
for(int i = 0, j = 0; i < s.length(); i++){
//i表示字符串s当前扫描到的位置,j表示模式串当前扫描到的位置
while(j > 0 && s[i] != p[j]){
//s[i]与p[i]不相等,直接将模式串p向后移动
/*
字符串s: * * a b c g k h f a b c d * * * * * * * * *
模式串P: a b c g k h f a b c g
a b c g k h f a b c g
a b c g k h f a b c g
a b c g k h f a b c g
对串s已经扫描的部分,我们应该
*/
j = next[j-1];
}
if(s[i] == p[j]){
j++;
}
if(j == p.length()){
cout << "match_end :" << i << endl;
j = 0;
}
}
}
int main(){
string s, p;
s = "abcdabcdebcd";
p = "bcd";
vector<int> next;
get_next(next, p);
match(s, p, next);
}