字符串匹配算法总结(RobinKarp、KMP、后缀数组)
这里主要整理了这三种常见的字符串匹配算法:① RobinKarp ② KMP ③ 后缀数组
一、RobinKarp算法(hash -> 滚动hash)
使用例子进行说明:
string S = “ABAAB”;
string p = “BAB“;
(这里 S 是原串,p 是模式串)
首先我们对模式串 p 进行hash,我们采用以下方法:
hash§ = (C0 × 31 + C1) × 31 + C2
(这里的 31 不是固定的,31 是素数,也可以选择其他素数)
如果觉得结果可能会越界,一定要记着进行取余。
加下来我们对原串 S 进行遍历,从每个位置开始进行 n = 3 长度的 hash 后开始比较。
到这里匹配算是完成了,但是我们来看看这样做的时间复杂度是怎么样的。
首先进行 hash 的时间复杂度为 O(n),对原串进行遍历的时间复杂度为 O(m)。
因此整体的时间复杂度为 O(mn),这对于暴力算法来说是没有什么提升的。
因此 RabinKarp 加了点预处理的味道,将复杂度降为 O(m+n)。
使用 hash 数组来保存原串 S 上每位开始进行 hash 的结果。
hash[0] = C0 × 312 + C1 × 31 + C2;
hash[1] = hash[0] - C0 × 312 + C3;
……
这样一来在计划完 hash§ 之后再扫一遍 hash 数组进行比对就可以得到结果。
但是这个算法中使用 hash 可能会有冲突!可能还需要另外的判断,比如逐位比对,
因此时间实际上 > O(m+n)。
(该算法在工业上并不常使用,但是可以用来检测抄袭,因为可以处理多模式匹配)
二、KMP算法(最常见)
利用模式串的前后缀特点创建 next 数组,利用 next 数组在原串中进行单向扫描就可以得到匹配结果。
时间复杂度为 O(m+n)。
三、后缀数组
什么是后缀数组?看下图。
将所有的后缀字符串取出来按照字典序进行排序就得到了后缀数组。
实际上后缀数组(suffix array)是排名 rank 与原始下标 index 的映射。
比如上图中 sa[0] = 0, sa[1] = 2, sa[2] = 4, sa[3] = 1, sa[4] = 2
有了后缀数组之后我们该如何进行匹配呢?
由于我们知道了后缀数组,这使得所有的后缀字符串按照字典序排序,因此可以使用二分法来匹配。
参考代码:
// 通过后缀数组匹配
int suffixFind(string str, string subStr){
int strLen = str.length();
int subLen = subStr.length();
Suffix *sa = new Suffix[strLen];
// 获得后缀数组
getSuffixArray(sa, str);
int left = 0;
int right = strLen;
int mid = 0;
// 二分查找
while(left <= right){
mid = (left+right)/2;
string temp;
// 如果长度大于模式串,则截取前缀
if(sa[mid].str.length() > subLen){
temp = sa[mid].str.substr(0, subLen);
}
else{
temp = sa[mid].str;
}
if(temp == subStr){
return sa[mid].index;
}
else if(temp > subStr){
right = mid-1;
}
else{
left = mid+1;
}
}
delete []sa;
return -1;
}
那么到底如何求得后缀数组呢?
① 暴力:使用结构体 Suffix 封装后缀字符串 str 以及对应的下标 index,在截取原串中所有的后缀字符串之后形成 n 个Suffix,再直接调用 STL 库中的 sort 函数即可。时间复杂度为 O(n2lgn)。
参考代码:
// 结构体封装后缀串以及原始下标 并且定义比较规则
struct Suffix{
string str;
int index;
bool operator < (Suffix other) const{
return str < other.str;
}
};
// 直接用结构体排序 O((n^2)logn)
void getSuffixArray(Suffix *sa, string str){
// 创建sa数组
for(int i = 0; i < str.length(); i++){
string sub = str.substr(i);
sa[i].str = sub;
sa[i].index = i;
}
// 排序
sort(sa, sa+str.length());
for(int i = 0; i < str.length(); i++){
cout << "str: " << sa[i].str << " index: " << sa[i].index << endl;
}
}
② 倍增法:空间换时间,需要使用辅助数组 rank 来进行。
rank 数组的含义与 sa 后缀数组的含义相反,rank 数组时原始下标 index 与排名 rank 之间的映射。
在上面的例子中 rk[0] = 0, rk[1] = 3, rk[2] = 1, rk[3] = 4, rk[4] = 2
(因此 rk[sa[i]] = i, sa[rk[i]] = i)
倍增法的基本思路:
1.当前以 k = 1 为长度在原串中进行截取字符串,并且进行排序,并且保存了各个字符串的 rank;
2.下一趟就进行以 2*k 为长度在原串中进行截取,并且利用上一趟保存的 rank 数组快速比较,更新 rank 数组;
3.之后反复执行步骤 2,直到 k 大于原串的长度为止。
因此可以判断这样以 2 的倍数进行增长排序的时间复杂度为 O(n(lgn)2)。
参考代码:
int *rk;
int change = 0; // 用来控制sort的比较规则
int k, strLen;
// 结构体封装后缀串以及原始下标 并且定义比较规则
struct Suffix{
string str;
int index;
bool operator < (Suffix other) const{
if(change == 0){
return str < other.str;
}
else{
// 如果前缀相同,比较后缀
if(rk[index] == rk[other.index]){
// 如果没有越界则调用上一轮的rank数组来得到结果
if(index+k/2 < strLen && other.index+k/2 < strLen){
return rk[index+k/2] < rk[other.index+k/2];
}
// 否则根据字符串的长度就可以得到结论
else{
return str.length() < other.str.length();
}
}
// 如果前缀不同直接返回结果
else{
return rk[index] < rk[other.index];
}
}
}
};
// 使用倍增法 O(n(logn)^2)
void getSuffixArray(Suffix *sa, string str){
// 创建rk排名数组
rk = new int[str.length()];
// 用长度1开始,不断倍增
for(int i = 1; i/2 <= str.length(); i *= 2){
k = i;
// 重新构造sa数组
for(int j = 0; j < str.length(); j++){
string sub;
if(j+i-1 < str.length()){
sub = str.substr(j, i);
}
else{
sub = str.substr(j);
}
sa[j].str = sub;
sa[j].index = j;
}
// 长度为1则直接sort
if(i == 1){
change = 0;
sort(sa, sa+str.length());
}
// 否则改变sort规则
else{
change = 1;
sort(sa, sa+str.length());
}
// 更新rank数组
int rank = 0;
rk[sa[0].index] = 0;
for(int j = 1; j < str.length(); j++){
if(sa[j].str == sa[j-1].str){
rk[sa[j].index] = rank;
}
else{
rk[sa[j].index] = ++rank;
}
}
}
delete []rk;
}
【END】感谢观看