Rabin-Karp字符串查找算法

简介
    暴力字符串匹配是字符串匹配算法中最基本的一种,它确实有自己的优点,比如它并不需要对文本(text)或模式串(pattern)进行预处理。然而它最大的问题就是运行速度太慢,所以在很多场合下暴力字符串匹配算法并不是那么有用。我们需要一些更快的方法来完成模式匹配的工作,然而在此之前,我们还是回头再看一遍暴力匹配,以便更好的理解其他字符串匹配算法。
    如下图所示,在暴力字符串匹配里,我们将文本中的每一个字符和模式串的第一个字符进行比对。一旦我们找到了一个匹配,我们就将文本中写一个字符取出来和模式串的第二个字符进行比较。
    
    暴力字符串匹配之所以慢,是因为它需要比对文本和模式串的每个字符。
    该算法运行速度慢的主要原因有二:
    一方面,我们需要对文本中的每个字符都进行比对;
    一方面,即使我们发现模式串首字符和文本中的某个字符相匹配,我们仍然需要对模式串中剩下的所有符号(字符)挨个进行比对,才知道它们是不是出现在接下来的文本中。
    那么,是否有别的方法可以用来判断文本是否包含模式串呢?

    答案是肯定的,确实存在一种更快的方法。为了避免挨个字符对文本和模式串进行比较,我们可以尝试一次性判断两者是否相等。因此,我们需要一个好的哈希函数(hash function)。通过哈希函数,我们可以算出模式串的哈希值,然后将它和文本中的子串的哈希值进行比较。这里有一个问题,我们必须保证该哈希函数能够对一个较长的字符串返回较短的哈希值。然而,我们又不能指望较长的模式串能得到较短的哈希值。但除此之外,这个新方法在速度上应该能比暴力法有显著提升。
    这种更快的方法就是Rabin-Karp算法。

概述
    Michael O. Rabin和Richard M. Karp在1987年提出一个想法,即可以对模式串进行哈希运算并将其哈希值与文本中子串的哈希值进行比对。总的来说这一想法非常浅显,唯一的问题在于我们需要找到一个哈希函数 ,它需要能够对不同的字符串返回不同的哈希值。例如,该哈希函数可能会对每个字符的ASCII码进行算,但同时我们也需要仔细考虑对多语种文本的支持。
        
    
    Rabin-Karp算法对模式串和文本中的子串分别进行哈希运算,以便对它们进行快速比对。
    
哈希算法可以有很多种不同的形式,它可能包含ASCII码字符以便对数字进行转化,但也可能是别的形式。我们唯一需要的就是将一个字符串(模式串)转化成为能够快速进行比较的哈希值。以"hello world"为例,假设它的哈希值hash('hello world')=12345。那么如果hash('he')=1,那么我们就可以说模式串"he"包含在文本"hello world"中。由此,我们可以每次从文本中取出长度为m(m为模式串的长度)的子串,然后将该子串进行哈希,并将其哈希值与模式串的哈希值进行比较。
    关于Hash函数的讲解来自于GeeksForGeeks【我添加】。
    
    
    
实现
    原文中Robin-Karp算法是PHP语言实现的,这里采用了C语言进行实现。具体代码如下所示。
  1. //Following program is a C implementation of the Rabin Karp Algorithm 
  2. //given in the CLRS book
  3. #include <stdio.h>
  4. #include <string.h>


  5. //is the number of characters in input alphabet
  6. #define d 256

  7. //pat -> pattern    txt -> text        q -> A prime number
  8. void search(char *pat, char *txt, int q)
  9. {
  10.     int m = strlen(pat);
  11.     int n = strlen(txt);

  12.     int i, j;
  13.     
  14.     int p = 0;        //hash value for pattern
  15.     int t = 0;        //hash value for txt
  16.     int h = 1;

  17.     //the value of h would be "pow(d, m - 1) % q"
  18.     for(= 0; i < m - 1; i++)
  19.     {
  20.         h = (* d) % q;
  21.     }

  22.     //calculate the hash value of pattern and first window of text
  23.     for(= 0; i < m; i++)
  24.     {
  25.         p = (* p + pat[i]) % q;
  26.         t = (* t + txt[i]) % q;
  27.     }

  28.     //slide the pattern over text one by one
  29.     for(= 0; i <= n - m; i++)
  30.     {
  31.         //check the hash values of current window of text and pattern
  32.         //if the hash values match then only check for characters on by one
  33.         if(== t)
  34.         {
  35.             //check for characters one by one
  36.             for(= 0; j < m; j++)
  37.             {
  38.                 if(txt[+ j] != pat[j])
  39.                 {
  40.                     break;
  41.                 }
  42.             }
  43.             if(== m)    //found
  44.             {
  45.                 printf("Pattern found at index %d\n", i);
  46.             }
  47.         }
  48.         //calulate hash value for next window of text: remove leading digit
  49.         //add trailing digit
  50.         if(< n - m)
  51.         {
  52.             t = (* ( t - txt[i] * h) + txt[+ m]) % q;

  53.             //we might get negative value of t, converting it to positive
  54.             if(< 0)
  55.             {
  56.                 t = t + q;
  57.             }
  58.         }
  59.     }
  60. }

  61. int main()
  62. {
  63.     char *txt = "Geeks For Geeks";
  64.     char *pat = "Geek";
  65.     int q = 101;                    //A prime number
  66.     search(pat, txt, q);
  67.     getchar();
  68.     return 0;
  69. }

多模式匹配
     Rabin-Karp算法非常适用于多模式匹配。事实上,它天生就是能够支持此类的操作,这也是它相对于其他字符串查找算法的优势。

算法复杂度
     Rabin-Karp算法的复杂度为O(mn),其中n和m分别是文本和模式串的长度。那么它到底比暴力匹配好在哪里呢?暴力匹配法的算法复杂度同样是O(mn),这样看起来Rabin-Karp算法在性能上并没有多大提升。
    然而在实际使用过程中,Rabin-Karp的复杂度通常被认为是O(n+m)。这就使得它比暴力匹配算法要快一些,具体见下图。
    
    Rabin-Karp的复杂度理论上是O(nm),但在实际使用中通常是O(n+m)。
    需要注意的是Rabin-Karp算法需要O(m)的预处理时间。
    【译者注】
    事实上,由于哈希函数无法保证对不同的字符串产生不同的哈希值,有哈希冲突的现象存在,所以即使模式串的哈希值和文本子串的哈希值相等,也需要对这两个长度为m的字符串进行额外的比对(当然,如果不相等也就不用比对了,其实大部分的时间省在这上面),这时比对的开销是O(m)。最坏情况下,文本中所有长度为m的子串(一共n-m+1个)都和模式串匹配,所以算法复杂度为O((n-m+1)m)。然而实际情况下,需要进一步比对的子串个数总是有限的(假设为c个),那么算法的期望匹配时间就变成O((n-m+1)+cm)=O(n+m)。

应用
    我们已经看到Rabin-Karp算法比暴力匹配法其实也快不了太多,那它的应用场景到底是哪里?
    【译者注】
    原文没有给出具体答案。要回答这个问题,需要先了解Rabin-Karp算法被称道和诟病的原因。然后根据自己的具体应用需要来做判断。

    Rabin-Karp算法被称道的三个原因:
    (1)它可以用来检测抄袭,因为它能够处理多模式匹配
    
    (2)虽然在理论上并不比暴力匹配法更优,但在实际应用中它的复杂度仅为O(n+m)
    (3)如果能够选择一个好的哈希函数,它的效率将会很高,而且也易于实现

    Rabin-Karp算法被诟病的两个原因:

    (1)有许多字符串匹配算法的复杂度小于O(n+m)
    (2)有时候它和暴力匹配法一样慢,并且它需要额外空间

结语

    Rabin-Karp算法之所以出众最大的原因就是它可以对多模型进行匹配。这一特性使得它在检测抄袭方面(尤其是大篇幅文字)非常好用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值