Rabin-Karp字符串查找算法

轉自:http://blog.chinaunix.net/uid-26548237-id-3968132.html (謝謝分享者!)

簡介

    暴力字符串匹配是字符串匹配算法中最基本的一種,它確實有自己的優點,比如它並不需要對文本(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算法之所以出衆最大的原因就是它可以對多模型進行匹配。這一特性使得它在檢測抄襲方面(尤其是大篇幅文字)非常好用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章