字符串匹配之Rabin-Karp算法
上一篇讲解了暴力匹配,暴力匹配最大的问题就是太慢了,而且太暴力了,不符合社会主义价值观。
因此,这里来讲解一个相对快一点的字符串匹配算法。
Rabin−Karp
R
a
b
i
n
−
K
a
r
p
算法是
Rabin
R
a
b
i
n
和
Karp
K
a
r
p
两位大佬在
1987
1987
年提出的。
为了能宏观的理解
Rabin−Karp
R
a
b
i
n
−
K
a
r
p
算法,这里先粗略的概括一下:
例如要在文本
T
T
中找模式串的位置,用
S
S
来表示偏移量,和
n
n
分别表示和
P
P
的长度
此算法就是通过某种映射关系,把字符串转化为数字,然后通过比较相应位置上数字是否相等,如果数字不相等,说明它所对应的字符串一定不相等,通过这样的排除来缩小要比较的字符串的范围。
下面来做具体说明:
为了便于说明,假设有一串字符是{}的集合,(这里假定以d为基数表示每一个字符,可以理解为d进制,其中d的大小为集合的长度,ps这里看不懂没关系,先向后看)
例如,上述的字符集在
0−9
0
−
9
的范围,可以表示为
10
10
进制的数。因此,字符串”
31415
31415
”对应的十进制数为
31415
31415
。
在比较数字之前,我们得先对文本串
T
T
和模式串做好相应的映射关系,对于文本串
T,S
T
,
S
每向后偏移一格,需要把
T[S..S+m]
T
[
S
.
.
S
+
m
]
的字符映射为一个数字
对于上图的映射,我们能在
O(m)
O
(
m
)
的时间计算出
31415
31415
,在
O(n−m)
O
(
n
−
m
)
的时间内映射出文本
T
T
每一个偏移所对应的数字
ts
t
s
。
ts+1=10(ts−10m−1T[s+1])+T[s+m+1] t s + 1 = 10 ( t s − 10 m − 1 T [ s + 1 ] ) + T [ s + m + 1 ]
例如: t2=10∗(23590−104∗2)+2=35902 t 2 = 10 ∗ ( 23590 − 10 4 ∗ 2 ) + 2 = 35902
因此,我们只要在 O(n−m+1) O ( n − m + 1 ) 的时间内遍历映射数组和 31415 31415 作比较,便能找出字符串匹配时的偏移量 S S 。
到这里,算法似乎已经接近尾声了,但是我们忽略了一个问题,映射出的数字太大以至于比较效率仍没有提高。解决的办法是,取一个合适的模(一般选素数), q q 的选取条件为:对于进制的字母表{ 0,1,...,d−1 0 , 1 , . . . , d − 1 },选取一个 q q 值,使得在一个计算机字长内,因此,映射的公式变为了:
ts+1=(d(ts−T[s+1]dm−1(modq))+T[s+m+1])modq t s + 1 = ( d ( t s − T [ s + 1 ] d m − 1 ( m o d q ) ) + T [ s + m + 1 ] ) m o d q
在加入取模 q q 后,映射的值相等并不能说明原来的字符串相等(不同的字符串可能映射出相同的值),但是可以通过这种关系排除大多数的不相等的字符串,从而降低字符串比较的代价。
//d为基数,q为素数
//处理一般ASCII常用字符,d取128,q取6999997
void RKSearch(const char* T,const char* P,int d = 128,int q = 6999997)
{
int n = strlen(T);
int m = strlen(P);
int h = ((int)pow((double)d, m - 1)) % q;
int p = 0, t = 0;
int i = 0;
for (; i < m; ++i)//预处理
{
p = (d*p + P[i]) % q;
t = (d*t + T[i]) % q;
}
int s = 0;
for (; s < n - m+1; ++s)//匹配
{
if (p == t)
{
int i = s, j = 0;
for (; j < m - 1;)//进一步筛选
{
if (T[i] == P[j]) ++i, ++j;
else break;
}
if (T[i] == P[j])
printf("%d\n", s);
}
if (s < n - m)//更新t为ts+1
t = (d*(t - T[s] * h%q+q) +( T[s + m])) % q;
}
}
上述代码中,表示字符集的个数,即 d d 进制,比如处理的字符在常用字符, d d 取,如果处理一般的 ASSIC A S S I C , d d 和可以直接取代码中的默认值。代码的第 19 19 行表示两个字符串取模相等的情况,第 21 28 21 28 行判断取模相等的字符串是否匹配,第 30、31 30 、 31 行为更新 ts。 t s 。
RK R K 算法的预处理时间为 O(m) O ( m ) ,最坏的情况下匹配时间为 O((n−m+1)m) O ( ( n − m + 1 ) m ) ,比如所每一个映射出的值都相等,这种情况比暴力算法还要多个预处理时间,这个算法的总体时间并不比暴力算法快多少,但它在综合情况下,对于某些情况很实用,比如说抄袭检测,只需要把库文件做一次预处理,就可以检测大量的模式串。
参考资料:《算法导论》