Rabin-karp算法是樸素字符串匹配算法的一個特例。當字母表∑爲d進制數時,即∑={0,1,2,…d-1}。如當d=10時字母表中的每個字符都是一個十進制數。我們在比較兩個長度爲m的子串時,可以把這兩個子串當作整數進行比較,而不用逐個字符比較,從而在某種程度上減少算法時間。
把一個由d進制數字組成的字符串轉換成相應的十進制整數,這是大家曾經都寫過的東西,一個可能的簡單實現如下:
int ConvertToInt(const char* str, int d)
{
int ans =0;
int m = strlen(str);
for(int i=0; i<m; i++)
{
ans = ans*d + str[i]-'0';
}
return ans;
}
這就是所謂的霍納法則(Horner’s rule)
對一個長度爲m的子串,求其對應的整數值,其複雜度爲O(m),如果不做一些特殊的處理,每次把子串和模式P比較時,先求子串對應的整數t,再與模式P對應的整數值p比較,算法的複雜度並沒有得到改進。如果用t(s)表示當前位移下子串T[s+1..s+m]的值,我們注意到把模式向右滑動一個窗口之後,下一個m位的子串T[s+2..s+m+1]對應的整數值t(s+1)和t(s)相比,只是去掉了最高位數字T[s+1],增加了一個最低位的數字T[s+m+1]。因而t(s+1)和t(s)之間有如下關係式:t(s+1)=d*(t(s)-T[s+1]*d^(m-1))+T[s+m+1]
例如對於文本十進制數字組成的文本”2359023141526739921”,d=10,m=5,當s=6時,t(s)=”31415”,T[s+1]=T[7]=3,T[s+m+1]=T[12]=2,所以
t(s+1) =10*(31415-3*10^4)+2=14152
因而我們初始時,只用求p=P[1..m],t0=T[1..m],t(s+1)的求值可以在每次循環迭代中去求,避免了多次函數調用的開銷。因而一個可能的實現如下:
void Bad_Rabin_Karp_Matcher(const char* T, const char* P, int d)
{
int n = strlen(T);
int m = strlen(P);
int h = pow(static_cast<double>(d),m-1);//h=d^(m-1)
int p=0;
int t=0;
for(int i=0; i<m; i++)
{
p = p*d + P[i]-'0';//p=P[0,m-1]
t = t*d + T[i]-'0';//t=T[0,m-1]
}
for(int s=0; s<=n-m; s++)
{
if(p==t)
{
cout<<"Pattern occurs with shift"<<s<<endl;
}
if(s<n-m)
{
t = d*(t-h*(T[s]-'0')) + T[s+m]-'0';
}
}
}
注:
(1) 在前面講解過程中數組下標從1開始,而在實際編程中數組下標是從0開始的,所以代碼和描述的有點差異,但是原理是一樣的。
(2)注意到我們求p,t,h的過程,不管用的是int還是long long型,當m增大時,肯定會產生溢出,比如說一個32位的int型表示的最大整數也就是20多億。
(3)一般做過一些ACM題目的同學,知道在這種情況我們可以通過對一個數求模來防止溢出,記其爲q。
(4)這裏又出現了一個問題,當p≠t (mod q)時,的確可以推出p≠q;但是當p=t (mod q)時,推不出p=t。即可能出現所謂的僞命中點,即p=t (mod q)但是p≠q。所以當p=t (mod q)時,我們要進行額外檢查,依次比較這m個字符,看其是否真的命中。
(5)通過把q設置爲一個較大的素數,可以有效的減少僞命中點數,因而可以減小額外檢查的開銷。
通過以上分析,改進後一個可能的算法實現如下:
void Rabin_Karp_Matcher(const char* T, const char* P, int d, int q)
{
int n = strlen(T);
int m = strlen(P);
int p = 0;
int t = 0;
int h = d;//當m=1時有問題,此處應該爲h=1,下面循環中k初始爲1
for(int k=2; k<m; k++)
{
h = h*d % q;
}
for(int i=0; i<m; i++)
{
p = (p*d + P[i]-'0') % q;
t = (t*d + T[i]-'0') % q;
}
for(int s=0; s<=n-m; s++)
{
// cout<<"p="<<p<<" t="<<t<<endl;
if(p==t)
{
if(strncmp(T+s,P,m)==0)
{
cout<<"Pattern occurs with shift"<<s<<endl;
}
}
if(s<n-m)
{
t = (d*(t-h*(T[s]-'0'))+T[s+m]-'0') % q;//此處也有問題,t可能小於0
}
}
}
但是運行結果和和預期不符,比如說你輸入P=”111111”,T=”1”,結果只輸出了一個位移值0,輸入T=”1234”,P=”34”居然沒有有效的位移輸出。
分析代碼,唯一可能的原因就是求餘過程出現到了問題,因此我們在循環中比較p和t的值之前先輸出它們的值,便於分析。果不其然,拿P=”111111”,T=”1”的輸出作爲示例,除了第一個位移p和t相等,後面的位移t居然是負值。也就是說對q求餘的結果可能出現[-q+1,-1]範圍的負值,爲了計算的正確性,我們要保證迭代的過程中t都爲正值。當t<0時,只需加上q即可。當然初始計算p和t的值時,p和t是不可能爲負值的。修改完代碼後,上面舉的第二個輸入用例運行正確,但是第一個仍然有問題,分析發現,原來當模式P只含一個字符時m=1,h=d^(m-1)=d^0=1,而前面我在求h時,只想到h是m-1個的相乘,把h初始化爲了d,疏忽了m可能取1,所以出現了此種情況。到此分析結束,完整正確的函數及測試代碼如下:
#include <cstdlib>
#include <iostream>
#include <cstring>
#include <cmath>
using namespace std;
void Rabin_Karp_Matcher(const char* T, const char* P, int d, int q)
{
int n = strlen(T);
int m = strlen(P);
int p = 0;
int t = 0;
int h = 1;
for(int k=1; k<m; k++) //預處理
{
h = h*d % q;
}
for(int i=0; i<m; i++)
{
p = (p*d + P[i]-'0') % q;
t = (t*d + T[i]-'0') % q;
}
for(int s=0; s<=n-m; s++)//匹配
{
cout<<"p="<<p<<" t="<<t<<endl;
if(p==t)
{
if(strncmp(T+s,P,m)==0)
{
cout<<"Pattern occurs with shift"<<s<<endl;
}
}
if(s<n-m)
{
t = (d*(t-h*(T[s]-'0'))+T[s+m]-'0') % q;
if(t<0)
{
t = t+q;
}
}
}
}
int main(int argc, char *argv[])
{
const int Max_Length = 1000;
const int d = 10;
const int q = 13;
char T[Max_Length];
char P[Max_Length];
while(gets(T))
{
gets(P);
Rabin_Karp_Matcher(T,P,d,q);
cout<<"next case:"<<endl;
}
system("PAUSE");
return EXIT_SUCCESS;
}
算法分析:預處理時間Θ(m),即求h,p,t的時間爲,匹配時間在最壞情況下爲Θ((n-m-1)m),因爲可能出現每次都是可能命中點的情況。如T=a^n,P=a^m,此種情況下驗證時間爲Θ((n-m-1)m)。當然實際中,可能的命中點一般很少。假設有c個,則算法的期望匹配時間爲O(n-m+1 +cm)=O(m+n),當m<<n時,期望匹配時間爲O(n).