1.字符串匹配:给定两个字符串S1, S2,我们需要确定S1里面是否包含S2,换句话说就是判断S2是否是S1的子串。
我们很容易想到的方法就是暴力搜索:
从第一个字符开始匹配,如果不相等则顺序后移一位,再进行比较,伪代码为:
n = s1.size
m = s2.size
for i = 0 to n - m
if s2[0 ... m-1] == s1[s ... s+m-1]
return s
这种方法的时间复杂度为O(n*m),很显然,这种方法不适用来解决大规模的匹配问题。
KMP算法:(时间复杂度为O(n + m))
现在来介绍KMP算法,这种算法能够省略那些重复的匹配,使用一个数组来保存相关的信息,并且利用这个数组就可以实现跳跃重复的匹配。
KMP算法的重点就是对这个数组进行初始化,在说明如何初始化这个数组之前,我们需要知道什么叫前缀,什么叫后缀
举个例子:在字符串ABABCAB中
所有的前缀为:
A AB ABA ABAB ABABC ABABCA
所有的后缀为:
B AB CAB BCAB ABCAB BABCAB
所以前缀就是除了最后一个字符的连续子串,后缀就是除了第一个字符的连续子串。
接着我们来说这个关键数组,我们把这个数组命名为next,那么对于字符串ABABCAB来说,这个数组为:
index 0 1 2 3 4 5 6
value 0 0 1 2 0 1 2
为什么能得到这个数组:
其中index代表数组的索引,也代表这个字符串的前 index + 1 个单词组成的子串:比如 index = 3,那么子串为ABAB,index = 4,那么子串为ABABC.
我们从index = 0 开始分析 :
index = 0,子串为 A
没有前缀,没有后缀
那么 value = 0;
index = 1,子串为 AB
前缀 : A 后缀 :B
没有相同的前缀和后缀,那么value = 0;
index = 2,子串为 ABA
前缀 : A AB 后缀 :A BA
前后缀相等的子串最大长度为1,那么value = 1;
index = 3,子串为 ABAB
前缀 : A AB ABA 后缀 :B AB BAB
前后缀相等的子串最大长度为2,那么value = 2;
index = 4,子串为 ABABC
前缀 : A AB ABA ABAB 后缀 :C BC ABC BABC
没有相同的前缀和后缀,那么value = 0;
index = 5,子串为 ABABCA
前缀 : A AB ABA ABAB ABABC 后缀 :A CA BCA ABCA BABCA
前后缀相同的最大子串长度为1,那么value = 1;
index = 6,子串为 ABABCAB
前缀 : A AB ABA ABAB ABABC ABABCA 后缀 :B AB CAB BCAB ABCAB BABCAB
前后缀相同的最大子串长度为2,那么value = 2;
这样就可以把数组初始化好了。
其中value代表着:如果我们匹配字符串时发生了不匹配,那么我们可以把字符串向前跳跃的步数。跳跃相当于把前缀代替成为后缀:
target : BCABABABCAB --> BCABABABCAB
pattern: ABABCAB --> ABABCAB
上面这个例子中:在C这里发生了不匹配,那么我们向后跳跃next[4] == 2步,为什么index是4?因为前面已经有4个字符完全匹配了。
现在知道了大致的匹配流程了,接下来就是需要书写代码来初始化这个数组,伪代码为:
size = pattern.size
next[size]
next[0] = 0
p = 0
for i = 1 to size
while p > 0 && pattern[i] != pattern[p]
p = next[p-1]
if pattern[i] == pattern[p]
p++
next[i] = p
上面最难理解的就是 p = next[p-1]了,这一句代表着当前面已经有着匹配时,下一个字符不匹配,那么模式自身就要退到上一次的状态,重新匹配上一次的状态的下一个字符。
初始化完毕数组之后就是kmp算法本身了, 伪代码为:
t = target.size
p = pattern.size
k = 0
for i = 0 to t
while k > 0 && target[i] != pattern[k]
k = next[k-1]
if pattern[k] == target[i]
k++
if k == p
return i-p+1;
return -1
两段代码很相似,因为两段代码都是对字符串进行匹配,只不过对数组初始化的时候是对模式本身进行匹配(寻找最大前后缀相同子串)
最后我们可以得到以下的c++代码:
kmp.hpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Kmp {
public:
Kmp(){}
~Kmp(){}
int getSolution(const string& total, const string& pattern);
private:
vector<size_t> next;
void initNext(const string& pattern);
};
int Kmp::getSolution(const string& total, const string& pattern){
initNext(pattern);
size_t t = total.size();
size_t size = pattern.size();
size_t p = 0;
for (size_t i = 0; i < t; ++i) {
while (p > 0 && total[i] != pattern[p]) { //当下一个字符不匹配时
p = next[p - 1];
}
if (total[i] == pattern[p]) {
p++;
}
if (p == size) {
return i - p + 1;
}
}
return -1;
}
void Kmp::initNext(const string& pattern){
size_t p = pattern.size();
next.resize(p, 0);
size_t k = 0;
for (size_t i = 1; i < p; ++i) {
while (k > 0 && pattern[i] != pattern[k]) { //当下一个字符不匹配时
k = next[k - 1];
}
if (pattern[i] == pattern[k]) {
k++;
}
next[i] = k;
}
}
main.cpp
#include "./kmp.hpp"
int main(int argc, char** argv) {
if (argc < 3) {
cout << "Usage: ./a.out total pattern" << endl;
return 0;
}
Kmp solution;
string total(argv[1]);
string pattern(argv[2]);
int result = solution.getSolution(total, pattern);
cout << result << endl;
return 0;
}
Mac 下面编译 : g++ -std=c++11 kmp.hpp main.cpp 通过。函数会输出第一次出现完全匹配时的target数组索引,如果没有匹配,则返回-1。