Rabin-Karp算法

本文讲了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算法的思想:

  1. 假设子串的长度为M,目标字符串的长度为N
  2. 计算子串的hash值
  3. 计算目标字符串中每个长度为M的子串的hash值(共需要计算N-M+1次)
  4. 比较hash值
  5. 如果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); 
        }
    }
}
  • 8
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值