KR ,Karp Rabin 算法

原文链接:http://blog.csdn.net/onezeros/article/details/5531354

Karp Rabin 算法是利用hash函数的特性进行字符串匹配的。

KR算法对模式串和循环中每一次要匹配的子串按一定的hash函数求值,如果hash值相同,才进一步比较这两个串是否真正相等

也许你会有这样的疑问,在Brute force暴力匹配中,每一次都把模式串和文本当前字串匹配,现在每一次都计算hash,还要进一步比较,会不会更慢呢?

答案肯定是不会啦,而且事实上KR算法效率很高

第一:不同子串hash值相同是小概率事件

第二:hash函数设计合理的情况下,计算速度相当快

第三:虽然理论上KR算法的时间复杂度是O(m*n),但现实应用中一般是O(m+n)

 

本文使用的hash函数如下:

hash(str[0..m-1])=(str[0]*2^(m-1)+str[1]*2^(m-2)+……+str[m-1]*2^0)mod q

q是一个较大的数,而且最好是素数,而且是大于m的素数。

计算时,*2的运算就使用移位代替了

 

在下面的实现代码中,q取整形最大值,也就是说,可以不用进行模运算了,这种偷工减料的做法只能在模式串很短的情形下才可以用,要不然会溢出的。粗略算了一下,如果模式串是ascii 英文字符,那么模式串长度不超过25的情况下,在32 位机上是没问题的

 

举例说一下KR算法吧:

在我写的这个总结中,各个算法使用的例子都一样,随便找的;

模式串 pattern="pappar"

文本串 text="pappappapparrassanuaragh";

pattern 长度记 pattern_len

预备阶段就是计算pattern的hash,长度为6,那么hash_pattern='p'*2^5+'a'*2^4+'p'*2^3+'p'*2^2+'a'*2^1+'r'*2^0

当然,这里使用的是字符的ascii值

也计算text前六个字符的hash,我们记第一个为hash_text[0]

然后就开始向前移动了,在移动时,要重新计算当前与模式串对应的串的hash值,这个工作叫rehash

初始化 i=0

如果 hash_pattern与hash_text[i]相等,返回 i

如果不等 计算新的hash值,就是text[i..i+patten_len]的hash,

当然这里不会像第一次那样全部计算,方法是

上一次计算的值减去上一次匹配时串的第一个字符乘以 2^pattern_len ,然后乘以2,再加上新加入比较的字符值

根据公式可以很清晰看出来。

就是减去计算中的第一项,把剩下的乘以2,然后在末尾加入新增的字符值

 

看代码吧,很简答的

[cpp]  view plain copy
  1. //Karp-Rabin algorithm,a simple edition  
  2. int karp_rabin_search(const char* text,const int text_len,const char* pattern,const int pattern_len)  
  3. {  
  4.     int hash_text=0;  
  5.     int hash_pattern=0;  
  6.     int i;  
  7.       
  8.     //rehash constant:2^(pattern_len-1)  
  9.     int hash_const=1;  
  10.     /*for (i=1;i<pattern_len;i++){ 
  11.         hash_const<<=1; 
  12.     }*/  
  13.     hash_const<<=pattern_len-1;  
  14.   
  15.     //preprocessing  
  16.     //hashing  
  17.     for (i=0;i<pattern_len;++i){  
  18.         hash_pattern=(hash_pattern<<1)+pattern[i];  
  19.         hash_text=(hash_text<<1)+text[i];  
  20.     }  
  21.   
  22.     //searching  
  23.     for (i=0;i<=text_len-pattern_len;++i){  
  24.         if (hash_pattern==hash_text&&memcmp(text+i,pattern,pattern_len)==0){  
  25.             return i;  
  26.         }else{  
  27.             //rehash  
  28.             hash_text=((hash_text-text[i]*hash_const)<<1)+text[i+pattern_len];  
  29.         }  
  30.     }  
  31.     return -1;  
  32. }       

hash函数的好坏会直接影响算法的效率,一般应遵循这样的规则:

1  要容易计算,本文中用的就不错,移位的速度大家是知道的

    而且在rehash,就是重新计算hash值时,hash的构造要能避免重新计算整个串的hash,而应该像本例中用到的那样,可以动态地很容易地更新

2 字符串hash值要尽量分布均匀,减少冲突,这是hash函数在任何场合的要求。做到这一点,就能减少匹配中字符的一个个比较,提高性能。如果能够保证每个串的hash值不同,就不用再比较字符了,可以省掉代码中的memcmp运算

   Monte Carlo改进的 RK算法就是只比较hash值,虽然那个改进的算法不能保证正确的结果,但以低于2.53/pattern_len的错误率,而很实用





转自 http://blog.csdn.net/touzani/archive/2007/05/30/1632149.aspx

 

字符串匹配(String matching)

问题的形式定义: 假设文本(Text)是一个长度为n的数组T[1…n], 模式(Pattern)是一个长度为 m ≤  n.的数组P[1..m];. 又假设P和T中的元素都属于有限字母表Σ 中的字符。 P和T常称为字符串。
如果0 ≤ s ≤ n – m 且T[s+1..s+m]=P[1…m], 则说P在T中出现且位移为s,此时成s为一个有效位移。
字符串匹配问题就变成在一个在一段制定的文本T中找出模式P的所有有效位移的问题。
 
下图给出了将要介绍的字符串匹配算法以及它们的预处理时间和匹配时间。
算法
预处理时间
匹配时间

朴素算法
0
O(( n -  m + 1) m)
Rabin-Karp
Θ( m)
O(( n -  m + 1) m)
有限自动机算法
O( m |Σ|)
Θ( n)
KMP算法
Θ( m)
Θ( n)
 
记号:
用Σ* 表示用字母表Σ中的所有有限长度的字符串的集合
字符串x的长度用| x |表示。
wy表示两个字符串w和y的连接。| wy | = | x |+| y |
 
 
字符串前缀  后缀

如果某个字符串 y ∈ Σ*,使得x=wy 。则称w是x的前缀, 记为w � x 。 如果w是x的后缀,记为w � x

 

可以把字符串匹配问题描述为 找出0 ≤ s ≤ n-m 并满足P � Ts+m的所有位移s

 
1.)朴素的字符串匹配算法
NAIVE-STRING-MATCHER(T, P)
1 n ← length[T]
2 m ← length[P]
3 for s ← 0 to n - m
4     do if P[1 ‥ m] = T[s + 1 ‥ s + m]         // 隐含着一个循环
5           then print "Pattern occurs with shift" s

 

时间复杂度为 O(( n -  m + 1) m), 如果 m = � n/2�. 那么时间复杂度为Θ( n 2), 这个算法效率不高,原因在于对于s的一个值,获得的关于文本的信息在考虑s的其他值时完全被忽略了。
例如,如果 P=aaab,设s=0 是有效的,那么 s=1, 2, 3 就不可能是有效位移,因为T[4]=b.
 
 
2.) Rabin-Karp字符串匹配算法,
 
实际应用中,Rabin和Karp建议的字符串匹配算法能较好地运行,还可以归纳出有关问题地其他算法,如二维模式匹配。
 
假定字符集Σ ={0, 1, 2, ……, 9}, 每一个字符对应一个十进制数字
(一般情况, 假定每个字符是基数为d的表示法中的一个数字, d=|Σ|。)可以用一个长度为k的十进制数字来表示由k个连续字符组成的字符串.

因此,字符串"31415" 对应于十进制数31415

已知模式P[1..m],设p表示其相应十进制数地值,类似地, 对于给定的文本T[1..n]. 用

ts 表示长度为m的子字符串  T[ s + 1 ‥  s +  m](  s = 0, 1, . . . ,  n –  m),  ts =  p 当且仅当 [ s + 1 ‥  s +  m] =  P[1 ‥  m]; 因此s是有效位移当且仅当  ts =  p. (暂不考虑p和 t可能是很大的数的情况)。

可以用霍纳规则(Horner’s rule) 在Θ(m) 的时间内计算p的值

p = P[m] + 10 (P[m - 1] + 10(P[m - 2] + · · · + 10(P[2] + 10P[1]) )).

 
Horner’s rule

 

类似地,可以在Θ( m)时间内,根据T[1..m]计算出 t 的值。

如果能在总共Θ(n - m + 1) 时间内计算出所有的t的值,那么通过把p值与每个ts(有n-m+1个)进行比较,就能够在Θ(m) + Θ(n - m + 1)= Θ(n) 时间内求出所有有效位移。(计算出1个t就跟p比较,处理结果。)

为了在Θ(n - m) 时间内计算出剩余的值t1t2, . . . , tn-可以在常数的时间内根据ts计算出ts+1,先看例子,假如m = 5,ts = 31415, 我们去掉高位数字T [s + 1] = 3,然后在加入一个低位数字T [s + 5 + 1](假设为2),得到:

ts+1 = 10(31415 - 10000 • 3) + 2 = 14152.

总结出公式:      ——公式1

如果预先计算出10 m -1(通过数论中的技术可以在 O(lg  m)完成, 在这里只需简单地在 O( m)时间内计算出就可以)。那么就可以在常数时间计算出 ts +1

因此,可以在Θ(m)时间内计算出p和t0然后在Θ(n - m + 1)时间内计算出t1, . . . , tn-m 并完成匹配。

现在来解决唯一的问题,就是计算中p和ts的值可能太大,超出计算机字长,不能方便地进行处理。如果p包含m个字符,那么, 关于在p上地每次算术运算需要“常数”时间这一假设就不合理了,幸运的是,对这一问题存在一个简单的补救方法,对一个合适的模q来计算p和ts的模,每个字符是一个十进制数,因为p和t以及公式1计算过程都可以对模q进行,所以可以在Θ(m)时间内计算出模q的p值,在Θ(n - m + 1)时间内计算出模q的所有ts值,通常选模q为一个素数,使得10q正好为一个计算机字长,单精度算术运算就可以执行所有必要的运算过程。 一般情况下,采用d进制的字母表{0, 1, . . . , d - 1}, 所选的q要满足d*q < 字长,调整公式1, 使其为:

其中的 h =  m -1 (mod  q)

但是加入模q后,由ts ≡ p (mod q)不能说明 ts = p. 但ts � p (mod q), 可以说明 ts ≠ p,

因此当 ts ≡  p (mod  q)时, 再用朴素的字符串匹配算法验证 ts =  p。. 如果q足够大,可以期望伪命中很少出现。
 
 
算法
RABIN-KARP-MATCHER(T, P, d, q)
 1 nlength[T]
 2 mlength[P]
 3 hdm-1 mod q
 4 p ← 0
 5 t0 ← 0
 6 for i ← 1 to m           � Preprocessing.
 7     do p ← (dp + P[i]) mod q
 8        t0 ← (dt0 + T[i]) mod q
 9 for s ← 0 to n - m       � Matching.
10     do if p = ts
11           then if P[1 ‥ m] = T [s + 1 ‥ s + m]
12                   then print "Pattern occurs with shift" s
13        if s < n - m
14           then ts+1 ← (d(ts - T[s + 1]h) + T[s + m + 1]) mod q
 
 
c++代码
 

void RABIN_KARP_MATCHER(string T, string P, int d, int q)  
/* 
搜索P在T中出现的位置
参数d :字母表的进制,亦即是字母表的元素个数
参数q : 一个较大的素数, 只需d*q < 字长
*/
{
    
    int n= T.length();
    int m= P.length();
    if( n < m)
        return ;
    int i, h=1;
    for(i=1; i<=m-1; i++)   // caculate h 
        h = h*d%q;

    int p=0, t=0;
    for(i=0; i<m; i++)      //  预处理,计算p, t0 
    {
        p = (( d*p + P[i]) % q); 
        t = (( d*t + T[i]) % q);
    }

    int s;
    for(s=0; s < n-m+1; s++)   //    匹配
    {
        if( p == t )
        {
            for(i=0; i<m; i++)        // 进一步验证
                if(P[i]!=T[s+i])
                    break;

            if(i==m)
                cout<<"Pattern ocurs with shift "<<s<<endl;
        }
        if( s < n-m )
            t= (  d* (t - T[s]*h%q + q) + T[s+m])  % q;  // 计算ts+1
    }
    cout<<"string matching ends"<<endl;
    return  ;
}

 

RABIN_KARP_MATCHER 预处理时间为 Θ (m) 匹配时间最坏情况下为Θ((n - m + 1)m),
因为Rabin_Karp算法跟朴素的字符串匹配算法一样,对每个有效位移进行显示验证,如果P = am and T = an, 则验证所需时间为Θ((n - m + 1)m), 因为n - m + 1个可能的位移中每一个都是有效位移。

实际应用中,有效位移数很少(常数c个),因此期望的匹配时间为O((n - m + 1) + cm) = O(n+m), 选取的素数q比p的长度m大得多。

通常处理ASCII码字符, d=128, 素数q可选6999997。

 

//*****************************************************************************************************************************

 

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAXN 100
const int d=128;
const int q=6999997;
int RK(char *T,char *pat)
{
    int n=strlen(T),m=strlen(pat);
    if(n<m) return -2;
    int i,s,t=0,p=0,h=1;
    for(i=1; i<m; i++) h=(h*d) % q; //h=d^m-1
    for(i=0,p,t; i<m; i++)
    {
        p = ( d*p+pat[i] ) % q; //p[m]
        t = ( d*t+T[i] ) % q; //t[m]
    }
    for(s=0; s<=n-m; s++)
    {
        if(p==t)
        {
            for(i=0; i<m; i++)
                if(pat[i] != T[s+i]) break;
            if(i==m) return s;
        }
        if(s<n-m)
            t = ( d*(t-h*T[s]) + T[s+m] ) % q; //t[s+1]
    }
    return -1;
}
char s1[MAXN],s2[MAXN];
int main()
{
    while(scanf("%s%s",s1,s2))
    {
        printf("%d\n",RK(s1,s2));
    }
    return 0;
}



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值