獨樹一幟的字串匹配演算法 RK演算法

2022-04-09 16:27:50 字數 4286 閱讀 5570

參加了雅虎2015校招,筆試成績還不錯,誰知初面第一題就被問了個字串匹配,要求不能使用kmp,但要和kmp一樣優,當時瞬間就呵呵了。後經過面試官的一再提示,也還是沒有成功在面試現場寫得。現將該演算法記錄如下,思想絕對是字串匹配中獨樹一幟的

字串匹配

存在長度為n的字元陣列s[0...n-1],長度為m的字元陣列p[0...m-1],是否存在i,使得sisi+1...si+m-1等於p0p1...pm-1,若存在,則匹配成功,若不存在則匹配失敗。

rk演算法思想

假設我們有某個hash函式可以將字串轉換為乙個整數,則hash結果不同的字串肯定不同,但hash結果相同的字串則很有可能相同(存在小概率不同的可能)。

演算法每次從s中取長度為m的子串,將其hash結果與p的hash結果進行比較,若相等,則有可能匹配成功,若不相等,則繼續從s中選新的子串進行比較。

假設進行下面的匹配:

s0s1

...si-m+1

si-m+2

...si-1

sisi+1

...sn-1

p0p1

pm-2

pm-1

情況一、hash(si-m+1...si) == hash(p0...pm-1),此時si-m+1...si與p0...pm-1有可能匹配成功。只需要逐字元對比就可以判斷是否真的匹配成功,若匹配成功,則返回匹配成功點的下標i-m+1,若不成功,則繼續取s的子串si-m+2...si+1進行hash

情況二、hash(si-m+1...si) != hash(p0...pm-1),此時si-m+1...si與p0...pm-1不可能匹配成功,所以繼續取s的子串si-m+2...si+1進行hash

可以看出,不論情況一還是情況二,都涉及乙個共同的步驟,就是繼續取s的子串si-m+2...si+1進行hash。如果每次都重新求hash結果的話,複雜度為o(m),整體複雜度為o(mn)。如果可以利用上乙個子串的hash結果hash(si-m+1...si),在o(1)的時間內求出hash(si-m+2...si+1),則可以將整體複雜度降低到線性時間

至此,問題的關鍵轉換為如何根據hash(si-m+1...si),在o(1)的時間內求出hash(si-m+2...si+1)

設計hash函式為:hash(si-m+1...si) = si-m+1*xm-1 + si-m+2*xm-2 + ... + si-1*x + si

則 hash(si-m+2...si+1) = si-m+2*xm-1 + si-m+3*xm-2 + ... + si*x + si+1 

= (hash(si-m+1...si) - si-m+1*xm-1) * x + si+1

hash結果過大怎麼辦?對某個大素數取餘數即可(經典方法),稱其為hashsize

所以,hash函式更新為:hash(si-m+1...si) = (si-m+1*xm-1 + si-m+2*xm-2 + ... + si-1*x + si) % hashsize

則 hash(si-m+2...si+1) = (si-m+2*xm-1 + si-m+3*xm-2 + ... + si*x + si+1) % hashsize

= ((hash(si-m+1...si) - si-m+1*xm-1) * x + si+1) % hashsize

設計演算法時需要注意的幾點:

1、可提前計算出hash(p0...pm-1)和xm-1並儲存

2、char c 的取值範圍為0~255,計算hash結果時會自動型別提公升為int,為避免符號位擴充套件,使用 (unsigned int)c & 0x000000ff

3、hash(si-m+1...si) - si-m+1*xm-1 的結果可能為負數,需先加上 si-m+1*hashsize 並最後 % hashsize 來保證結果非負

具體**如下:

1

#define unsigned(x) ((unsigned int)x & 0x000000ff)

2#define hashsize 1000001934

int hashmatch(char* s, char*p)

18 i = m - 1;19

do27 i++;

28if (i >=n)

29break;30

//o(1)時間更新s子串的hash結果

31 sv = (sv + unsigned(s[i - m]) * (hashsize - base)) %hashsize;

32 sv = (sv * 10 + unsigned(s[i])) %hashsize;

33 } while (i

3435

return -1

;36 }

時間複雜度分析:迴圈複雜度o(n),hash結果相等時的逐字元匹配複雜度為o(m),整體時間複雜度為o(m+n)。空間複雜度為o(1)執行時間pk

隨機生成10億位元組(1024*1024*1023)的字串儲存到檔案num.txt中,讀出到字串s中,p長度為1024*10位元組,分別使用rk演算法和kmp演算法進行實驗

從檔案num.txt中讀取字串到s中所需時間為:

匹配成功時,rk演算法匹配所需時間為:

匹配成功時,kmp演算法匹配所需時間為:

匹配不成功時,rk演算法匹配所需時間為:

匹配不成功時,kmp演算法匹配所需時間為:

演算法優化

在上面的測試中rk演算法還是慢於kmp的,優化從兩點出發:一是用其他運算代替取模運算,二是降低hash衝突。

先解決降低衝突的問題,在之前的**中,我們使用了x=10,假設存在char值為2,20,200的三個字元a,b,c,可以發現a*1000,b*100,c*10的hash結果是相同的,也就是發生了衝突,所以取大於等於256的數做x則可以避免這種衝突。另外hashsize的大小也會決定衝突發生的概率,hashsize最大可以多大呢?對於unsigned int來說,總共有2^32次方個,所以可以取hashsize為2^32次方。而計算機對於大於等於2^32次方的數會自動捨棄高位,其剛好等價於對2^32次方取模,即對hashsize取模,所以便可以從**中去掉取模運算。

優化後的**如下(**中d即上文中的x):

1

#define unsigned(x) ((unsigned char)x)

2#define d 25734

int hashmatch(char* s, char*p)

19 i = m - 1;20

do28 i++;

29if (i >=n)

30break;31

//o(1)時間內更新sv, sv + unsigned(s[i - m]) * (~base + 1)等價於sv - unsigned(s[i - m]) * base

32 sv = (sv + unsigned(s[i - m]) * (~base + 1)) * d +unsigned(s[i]);

33 } while (i

3435

return -1

;36 }

匹配成功時,優化後rk演算法匹配所需時間為:

匹配不成功時,優化後rk演算法匹配所需時間為:

可以看出,優化後的rk演算法已經在時間上優於kmp了。而且大小為2^32次方的hashsize也保證了s的10億個子串基本不會發生衝突。

字串模式匹配 BF演算法和RK演算法

bf演算法 暴力匹配演算法,也叫樸素匹配演算法 效能不是很高。我們在主串中,檢查起始位置分別是0.1.2 n m且長度為n m 1個子串,看有沒有跟模式串匹配的。在a中查詢b,a就是主串,b就是模式串,且a b 最壞的時間複雜度為o n m 但是實際上,這也是常用的,1.模式串和主串的長度都不會太長...

字串匹配經典算

最近在刷資料結構,看到了字串匹配演算法kmp,bm,kp等,感覺是面試中應該要會的知識點,就先記錄下來了,方便之後的複習檢視 1.kmp演算法 kmp演算法是在暴力演算法之上做了一些改進,不會重複的比對當前比對失敗的字首,即利用了匹配串本身的資訊來構造乙個查詢表next,該錶能夠指導當次匹配失敗下,...

字串匹配之RK演算法 學習筆記

rk演算法是rabin karp演算法的簡稱,是經典的字串匹配演算法,在 演算法導論 上是有介紹的,有興趣的同學可以去看看。rk演算法的複雜度可以說是比上不足比下有餘,比一般的匹配演算法要好,但是又比不上kmp,sunday等演算法。演算法表現跟快排比較相似,演算法平均複雜度表現較好,但最壞情況時複...