单模匹配问题
单模匹配问题:一个字符串和另一个字符串之间的包含关系确认的问题,
假设求解字符串A中是否包含字符串B。那么字符串A就是文本串,字符串B就是模式串。
也就是求解文本串中是否含有模式串的问题。
前置条件:
文本串的长度为n;
模式串的长度为m;
n>=m>=0;
RK算法
Rabin-Karp算法:就是利用hash思想,我们通过哈希算法对主串中的 n-m+1 个子串分别求哈希值,然后逐个与模式串的哈希值比较大小。如果某个子串的哈希值与模式串相等,那就说明对应的子串和模式串匹配了。
由于模式串B的长度为m,那么如果能比配的上文本串A,也就是找到文本串中的一段长度为m的子串。
RK算法就是利用文本串中每一个长度为m的子串对应hash出来的键值,来获得匹配能力。
存在的问题
hash本身纯在的问题:hash冲突.
hash函数的确定。有关常用的hash函数见链接
简单实现
// “RK.cpp"
#include<iostream>
#include<climits>
#include<string>
#include<unordered_map>
using namespace std;
long long int hash_func(string s){
int n = s.size();
long long key = 0;
for(int i = 0; i < n; ++i){
if(key > LONG_LONG_MAX - s[i]) key = key % LONG_LONG_MAX;
key += s[i] - 'a';
}
return key;
}
bool single_catch(string &A,string &B){
int n = A.size();
int m = B.size();
if(n < m) return false;
int start = 0,end = m - 1;
unordered_map<long long,int> cap;
long long key = 0;
while(end < n){
key = hash_func(A.substr(start,m));
if(cap.count(key) == 0) cap[key] = start;
start++;
end = start + m - 1;
}
long long need = hash_func(B);
if(cap.count(need)){
if(A.substr(cap[need],m) == B) {
return true;
}
}
return false;
}
int main(){
string A,B;
cin >> A >> B;
if(single_catch(A,B)) cout << "I catch it !!"<<endl;
else cout << "sorry,a worry string showed!!"<<endl;
return 0;
}
BM算法
Boyer-Moore算法:
RK和暴力匹配算法都是每次往后移动一位,而BM算法可以按照规则每次往后移动好几位。这匹配规则可以是坏字符方法和好后缀方法。
坏字符方法
原则是从end向start方向匹配。
以文本串A=”abccdssdff“
和模式串B="ccds"
的匹配为话题展开。其中n=10,m=4;
我们设定文本串中被匹配字符子串的起始下标为start,结束下标为end。
文本串: abccdssdff
模式串: ccds
start = 0,end = 3;
此时发现c
和s
匹配不上,坏字符就是c
,定义其下标为i
=3。那么坏字符之前的子串肯定匹配不上。就需要移动start。
移动start,此时分两种情况:
- 坏字符在模式串B中存在,找到模式串最右的下标即ccdc中的下标index = 1.即start = i - index; end = start + m;
- 坏字符不存在,start = end + 1;end = start + m; 直接移动到坏字符后面匹配。
文本串: abccdssdff
模式串: ccds
start=2;end=5;
此时匹配上。
warning:
上面就是使用坏字符规则来匹配,但是这也会有极端情况,比如主串aaaaaaaaaaaaaaaa,模式串baaa,这时候使用坏字符规则,会发现倒着移动了,可以解决的是每次增加一个判断,如果重新找到的start 比之前的小,就赋值start为之前的值加1.不过这还是比较麻烦的。
这时候就需要用到好后缀规则了。
好后缀方法
原则是从end向start方向匹配。
以文本串A=”abcacabcbcbacabc“
和模式串B="cbacabc"
的匹配为话题展开。其中n=16,m=7;
我们设定文本串中被匹配字符子串的起始下标为start,结束下标为end。
文本串: abcacabcbcbacabc
模式串: cbacabc
start = 0,end = 6;
此时end开始匹配没找到好后缀,start = start + 1; end = start + m;
文本串: abcacabcbcbacabc
模式串: cbacabc
start = 1,end = 7;
找到好后缀”acabc"
,但是到b和c匹配不了了,此时后移start的规则如下。
移动start,此时分两种情况:
- 好后缀在模式串B中存在,找到模式串最左的下标,令好后缀和模式串中的最左子串进行匹配后,start置位。
- 好后缀的后缀(c,bc,abc,cabc等)在B中存在,找到模式串最左的下标,令好后缀和模式串中的最左子串进行匹配后,start置位。
- 都不存在,start = end + 1;end = start + m;
文本串: abcacabcbcbacabc
模式串: cbacabc
- 此时发现最左只能匹配到好后缀的后缀c,此时
start = 7,end = 14;
没匹配上start = start + 1;
文本串: abcacabcbcbacabc
模式串: cbacabc
- start = 8,end = 15;没匹配上start = start + 1;
文本串: abcacabcbcbacabc
模式串: cbacabc
- start = 9,end = 16;此时匹配上。
坏字符+好后缀配合。
坏字符存在问题:
可能会倒退start,产生越界问题。
好后缀存在问题:
一旦好后缀没匹配上,那么每次都会start = start+1,极端情况下会变成BF匹配算法(Brute Froce),即暴力匹配,本文不涉及暴力匹配。
所以在BM算法中可以同时以模式串构建最右hash(坏字符规则每次匹配模式中最右出现的该字符)和最左hash(好后缀每次匹配最左后缀或最左后缀后缀)。计算两个规则移动更加靠后的来后移start。
有兴趣大侠可以课下实现以下。
Sunday算法
sunday算法也是每次移动好几个位,区别是sunday是从start向end方向去匹配的。
该算法与BM算法类似,不做细讲。
可以查看链接讲解,讲的挺好的。
这里我根据链接的讲解写了一个代码,可以参考一下:
// Sunday.cpp"
#include<iostream>
#include<string>
#include<vector>
using namespace std;
//26个字母,仅包括小写字母的字符串单模匹配。可以自己拓展。
vector<int> hasht;
void func(string s){
int len = s.size();
for(int i = 0; i < len; ++i){
hasht[s[i]-'a'] = i;
}
return;
}
string Sun(string &A,string &B){
int n = A.size(),m=B.size();
int start = 0, end = start + m - 1;
while(end < n){
int i = start;
while(A[i] == B[i]) ++i;
if(i > end) return A.substr(start,m);
if(end == n - 1) return "";
int index = hasht[A[end + 1]];
if(index == -1)start = end + 1;
else start = end + 1 - index;
end = start + m - 1;
}
return "";
}
int main(){
string A,B;
cin >> A >> B;
hasht = vector<int>(26,-1);
func(B);
cout << Sun(A,B);
return 0;
}
SHIFT-AND算法(位运算)
shift-and算法也可以解决字符串单模匹配问题,不过shift-and算法常用于多模匹配;
可以看一下2016ACM/ICPC亚洲区大连站
将模式串转换位二进制数来参与匹配的方法。
文本串:aecaeaecaed
模式串:aecaed
- 构建模式串二进制数:
即
d[a] = 001001;
d[c] = 000100;
d[d] = 100000;
d[e] = 010010;
-
构建判断二进制数P,初始化为0。
P二进制数的意义,P的某位index为1,意味着:
当前文本串遍历的前index位(以当前遍历位置为结尾)能匹配到模式串的前ndex位前缀。 -
P二进制数的迭代,
P = (P << 1 | 1) & d[s[i]]
运转过程如下:
P = 0;P初始化为00000000;
for(int i = 0; i < n; ++i); 令i从文本串S起始位开始遍历匹配。
- i = 0时, s[i] = a; d[a]= 001001;
(P << 1 | 1) 是 00000001,此时与d[s[i]]即d[a]进行与得到P = 00000001;
解释:当前i=0,且P的第一位为1,即当前遍历位置往前一位可以匹配到模式串的前一位。- i = 1时,s[i]=e;P=00000010;
解释:当前i=1,且P的第二位为1,即当前遍历位置往前二位可以匹配到模式串的前二位。- i = 2,P=00000100;
- i = 3, P = 00001001;
- i = 4 ; P = 00010010;
- I = 5; P = 00000001;
- I= 6; P = 00000010;
- I = 7; P = 00000100;
- I = 8; P = 00001001;
- I = 9; P = 00010010;
- I = 10;P= 00100000;此时匹配上了(即P == 1 << m);
可以自己理解一下:
KMP算法
KMP算法是字符串单模匹配算法的高频面试点。其思想与BM和Sunday相似,都是每次后移几位。
关键点是next数组的构建。
这个讲解的也有很多,链接放在这里 ,最好自己实现一下。
next数组实现方法:
public static int[] next(String t) {
int[] next = new int[t.length()];
next[0] = -1;// 这里规定next[]第1个元素值为-1
next[1] = 0;
int j = 2;// 从给定的字符串的第3个字符(下标从0开始)开始计算next[j]数值
while (j < t.length()) {// 从第三个字符开始逐次求解对应的next[]值
if (t.charAt(next[j - 1]) == t.charAt(j - 1)) {// 判断Y与X是否相等(X,Y对应上图,下同)
next[j] = next[j - 1] + 1;// 若Y与X相等,当前next[j]为上一个next[]数组值加1
j++;// 开始自增一个字符,准备下一次求解
} else if (t.charAt(0) == t.charAt(j - 1)) {// 若不相等,判断X与子串(模式串)第一个字符是否相同
next[j] = 1;// 若相同,找到一个长度为1的相同前后缀,即next[j]为1
j++;
} else {// 若上述两个条件都不满足,即意味着没有相等的前后缀,即next[j]为0
next[j] = 0;
j++;
}
}
return next;
}