1.RK简单描述
这是由Rabin和Karp提出的字符串匹配(检索)算法,即称RK算法。RK算法的基本思想是按字符串的Hash值查找,即:
计算出两个字符串的Hash值,如果两个字符串hash的值不相同,则它们肯定不相同,已经没有去比较字符串内容的必要,如果它们hash后的值相同,它们不一定相同,但是可以通过比较内容去确定两个字符串是否相同。
RK算法的基本思想就是:将目标字符串target的hash值跟主串mainString中的每一个长度为target.length的子串的hash值比较。如果二者hash值不同,则它们肯定不相等;如果二者hash值相同,则再逐位比较每一个字符。
计算目标字符串的Hash值: 这里有很多形式,我暂时没有什么好的方法,直接计算字符对应的ASCII码值,当然为了避免加起来的值太大,加的是每个字符减掉’A’。 计算如下:
int targetHash = 0;
for(char c: target) {
targetHash += c – 'A';
}
这样的话,如果字符全部是由ASCII码小于128且大于65的字符组成,最少可以计算的target的长度为: len = 2147483647 / (127 – 65) = 34636833(接近3500万(个字符)的长度)。 最多就是2147483647(21亿个字符)了,即字符全是’B’的时候。(int为-231—231-1)
而主串长度为target.length的子串p计算初始时即是从首位0到target.length-1处的Hash值,巧妙的是如果匹配不成功,往后的计算都是减掉p的第一位,然后从主串中p末位的后一位加进来即可。即主串每次都是往后移动一位,没有回溯。
我的话说得不怎么明白,且看如下字符串。
串 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|
S | a | b | c | a | b | c | m | n | b | c | T | b | c | m | n | | | | | | |
(1)计算T的hash值:Thash = ‘b’- ‘A’ + ’c’ - ‘A’ +’m’ - ‘A’ +’n’ - ‘A’ = 98 – 65 + 99 – 65 + 109 – 65 + 110 – 65 = 156;(代码中通过循环实现) (2)计算S第一个子串P的hash值:Phash = ‘a’ - ‘A’ +’b’ - ‘A’ +’c’ - ‘A’ +’a’ - ‘A’ = 97 – 65 + 98 – 65 + 99 – 65 + 97 – 65 = 131;(注意每次都减掉’A’,不然字符串过长是,很容易溢出)。 (3)比较T和P的hash值,因为二者不相等,所以,直接移动主串,即更新P的hash值,更新:Phash -= S[0] – ‘A’,Phash += s[4] – ‘A’,即先减掉P的首位,再加上字符串P在S中后一位的hash值,上述可简化为Phash = Phash – (S[0] – ‘A’) + (s[4] – ‘A’),再化简就是Phash = Phash – S[0] + s[4] = Phash – (S[0] - s[4]),结果为Phash -= S[0] - s[4];Phash = 132; (4)因为Thash = 156不等于132,所以Phash -= S[1] – S[5],Phash = 133; (5)因为Thash = 156不等于133,所以Phash -= S[2] – S[6],Phash = 143; (6)因为Thash = 156不等于143,所以Phash -= S[3] – S[7],Phash = 156; (7)Thash = 156等于Phash = 156,且此时P = “bcmn” == T = “bcmn”,匹配成功。
可以看出过程简单,而且不需要主串回溯,就感觉很行,而且避免了不必要的比较。
2.代码解说
这是全部代码。
#include <iostream>
using namespace std;
#include <string>
int rabinKarp(string mainString, string target);
bool isMacth(string s, int i,string p,int m);
void rabinKarpTest();
int main() {
rabinKarpTest();
return 0;
}
int rabinKarp(string mainString, string target) {
int mlen = mainString.size();
int tlen = target.size();
int targetHash = 0;
int mainSubStrHash = 0;
for(int i = 0;i < tlen;i++) {
targetHash += (target[i] - 'A');
mainSubStrHash += (mainString[i] - 'A');
}
for(int i = tlen;i < mlen + 1;i++) {
if(targetHash == mainSubStrHash && isMacth(mainString,i - tlen,target,tlen)) {
return i - tlen;
}
mainSubStrHash -= mainString[i - tlen] - mainString[i];
}
return -1;
}
bool isMacth(string s, int i,string p,int m) {
for(int j = i,k = 0;j < i + m && k < m;j++, k++) {
if(s[j] != p[k]) {
return false;
}
}
return true;
}
void rabinKarpTest() {
string mainString;
string target;
int contin = 1;
do {
system("cls");
cout << "当前检测的是RK(RabinKarp)算法\n" << endl;
cout << "请输入mainString:";
cin >> mainString;
cout << "请输入target:";
cin >> target;
int result = rabinKarp(mainString, target);
if(result != -1) {
cout << target << "的起始位置在" << mainString << "的" << result << "处" << endl;
}
else {
cout << target << "不在" << mainString << "中" << endl;
}
cout << "继续检测输入1,否则输入0:";
cin >> contin;
} while (contin == 1);
cout << "欢迎再次检测!" << endl;
}
先计算target的hash值以及主串第一个长度为target.length的子串的hash值,默认tlen <= mlen,因为我们可以先判断target的长度是否大于mainString的长度,大于那肯定就匹配不成功了。
int targetHash = 0;
int mainSubStrHash = 0;
for(int i = 0;i < tlen;i++) {
targetHash += (target[i] - 'A');
mainSubStrHash += (mainString[i] - 'A');
}
下面说说为什么循环条件是mlen + 1,即主串长度加一。 比如mainString = “123”,target = “23”, 进入循环时i = 2,然后匹配是不成功的,所以更新hash值后,i++,此时 i为3,那么i就等于主串的长度了,如果条件是mlen的话,就不匹配了。
for(int i = tlen;i < mlen + 1;i++) {
if(targetHash == mainSubStrHash && isMacth(mainString,i - tlen,target,tlen)) {
return i - tlen;
}
mainSubStrHash -= mainString[i - tlen] - mainString[i];
}
3.结语
RK算法是除了BF算法之外的匹配算法,我一看就懂的,特别容易理解,而且写一遍代码之后,就已经记住了,而且原理也很清晰明了。当然了,我个人表述应该不是很明了,哈哈。 其实不会的东西我有个习惯就是先知道答案再去做去理解,这也有效果的,这也不失为一种学习方法,自己苦苦思索很久,有时候也不太明智。
|