本文讲了Rabin-Karp算法,包含C++中的代码实现。
先讲一下Rabin-Karp算法,它是字符串快速查找的一种算法,解决思路是把一个字符串,看作是字符集长度进制的树,如果是ASCII,这个进制就是128,如果是只考虑英文小写字母,那这个进制就是26,通过数值的比较得出字符串的比较结果。
在这里,要先将一个背景知识,也就是朴素的模式匹配算法。它是把要查找的内容,一步一步的与源文(这里指被查找的内容)进行比较,如果匹配失败,主串和子串回溯,字符串的位置自增1,然后再继续重新匹配。伪代码实现如下
For i=1 to 主串的length - 模式串的length + 1
For j=1 to 模式串的length
伪代码:
NAIVE_STPRING_MATCHER(T,S)
n = T.length;
m = S.length;
for(s=0;s <= n-m;s++)
if S[1..m]==T[s+1..s+m];
print "Pattern occurs with shift" s;
两个for来解决,那它的时间复杂度就是O((n-m+1)*m)
在模式串较小的情况下,时间复杂度为O(mn)
。
C++的代码实现是:
#include <iostream>
using namespace std;
void search(char *S, char *T) //S search sequence, T target sequence
{
int M = strlen(S); //可以重写strlen的实现
int N = strlen(T);
/* A loop to slide S[] one by one */
for (int i = 0; i <= N - M; i++)
{
int j;
/* For current index i, check for pattern match */
for (j = 0; j < M; j++)
{
if (T[i+j] != S[j])
break;
}
if (j == M) // if S[0...M-1] = T[i, i+1, ...i+M-1]
{
cout << "Pattern found at index " << i << endl;
}
}
}
int main()
{
char *T = "AABAACAADAABAAABAA";
char *S = "AABA";
search(S, T);
return 0;
}
通过分析朴素字符串匹配算法,我们发现了一个问题,朴素算法会把前一次的匹配信息丢掉,然后从头再来,这样浪费资源,也增加了时间成本。由于完成两个字符串的比较需要对其中包含的字符进行逐个比较,所需的时间较长,而数值比较则一次就可以完成,那么我们首先把“搜索词”中各个字符的“码点值”通过计算,得出一个数值(这个数值必须可以表示出字符的前后顺序,而且可以随时去掉某个字符的值,可以随时添加一个新字符的值),然后对“源串”中要比较的部分进行计算,也得出一个数值,对这两个数值进行比较,就能判断字符串是否匹配。对两个数值进行比较,速度比简单的字符串比较快很多。
如果我们要在 ASCII 字符集范围内查找“搜索词”,由于 ASCII 字符集中有 128 个字符,那么 M 就等于 128,比如我们要在字符串 “abcdefg” 中查找 “cde”,那么我们就可以将搜索词 “cde” 转化为“("c"的码点 * M + "d"的码点) * M + "e"的码点 = (99 * 128 + 100) * 128 + 101 = 1634917
”这样一个数值。
分析一下这个数值:1634917,它可以代表字符串 “cde”,其中:
代表字符 “c” 的部分是“ "c"的码点 * (M 的 n - 1 次方) = 99 * (128 的 2 次方) = 1622016
”
代表字符 “d” 的部分是“ "d"的码点 * (M 的 n - 2 次方) = 100 * (128 的 1 次方) = 12800
”
代表字符 “e” 的部分是“ "e"的码点 * (M 的 n - 3 次方) = 101 * (128 的 0 次方) = 101
”
(n 代表字符串的长度)
我们可以随时减去其中一个字符的值,也可以随时添加一个字符的值。
“搜索词”计算好了,那么接下来计算“源串”,取“源串”的前 n 个字符(n 为“搜索词”的长度)”abc”,按照同样的方法计算其数值: ("a"的码点 * M + "b"的码点) * M + "c"的码点 = (97 * 128 + 98) * 128 + 99 = 1601891
然后将该值与“搜索词”的值进行比较即可。比较发现 1634917 与 1601891 不相等,则说明 “cde” 与 “abc” 不匹配,则继续向下寻找,下一步应该比较 “cde” 跟 “bcd” 了,那么我们如何利用前一步的信息呢?首先去掉 “abc” 的数值中代表 a 的部分: (1601891 - "a"的码点 * (M 的 n - 1 次方)) = (1601891 - 97 * (128 的 2 次方)) = 12643
然后再将结果乘以 M(这里是 128),再加上 “d” 的码点值不就成了 “bcd” 的值了吗:
12643 * 128 + "d"的码点 = 1618304 + 100 = 1618404
这样就可以继续比较 “cde” 和 “bcd” 是否匹配,以此类推。
Rabin-Karp算法的思想:
- 假设子串的长度为M,目标字符串的长度为N
- 计算子串的hash值
- 计算目标字符串中每个长度为M的子串的hash值(共需要计算N-M+1次)
- 比较hash值
- 如果hash值不同,字符串必然不匹配,如果hash值相同,还需要使用朴素算法再次判断
为了快速的计算出目标字符串中每一个子串的hash值,Rabin-Karp算法并不是对目标字符串的 每一个长度为M的子串都重新计算hash值,而是在前几个字串的基础之上, 计算下一个子串的 hash值,这就加快了hash之的计算速度,将朴素算法中的内循环的世间复杂度从O(M)将到了O(1)。
d取字符集的个数
#define d 256
/* S -> Search Sequence
T -> Target Sequence
q -> A prime number
*/
void search(char *S, char *T, int q) {
int M = strlen(S);
int N = strlen(T);
int i, j;
int SHashValue = 0; // hash value for Search Sequence
int THashValue = 0; // hash value for Target Sequence
int h = 1; // h 的计算公式:pow(d, M-1) % q
//计算h
for (i = 0; i < M-1; i++)
h = (h * d) % q;
// Calculate the hash value of Search Sequence
for (i = 0; i < M; i++) {
SHashValue = (d * SHashValue + S[i]) % q;
THashValue = (d * THashValue + T[i]) % q;
}
// Slide the pattern over text one by one
for (i = 0; i <= N - M; i++) {
// Chaeck the hash values of current T and S
// If the hash values match then only check for characters on by one
if ( SHashValue == THashValue ) {
/* Check for characters one by one */
for (j = 0; j < M; j++) {
if (txt[i+j] != pat[j])
break;
}
if (j == M) { // if SHashValue == THashValue and S[0...M-1] = T[i, i+1, ...i+M-1]
cout << "Pattern found at index " << i << endl;
}
}
// Calulate hash value for next text: Remove leading digit,
// add trailing digit
if ( i < N-M )
{
THashValue = (d * (THashValue - T[i] * h) + T[i+M]) % q;
// We might get negative value of THashValue, converting it to positive
if(THashValue < 0)
THashValue = (THashValue + q);
}
}
}