1. 程式人生 > 實用技巧 >子字串匹配常用演算法總結

子字串匹配常用演算法總結

前言

新開專欄【資料結構拾遺】

本專欄旨在快速瞭解常見的資料結構和演算法。在需要使用到相應演算法時,能夠幫助你回憶出常用的實現方案並且知曉其優缺點和適用環境。

參考

子字串匹配

子字串匹配演算法的定義:

  • 文字長度:N
  • 模式字串長度:M
  • 有效位移:s

解決字串匹配的演算法有非常多,目前常用的有以下幾種:

  • 暴力查詢
  • KMP 演算法
  • Boyer-Moore演算法
  • Rabin-Karp指紋字串查詢

字串匹配演算法通常分為兩個步驟:預處理(Preprocessing)和匹配(Matching)。所以演算法的總執行時間為預處理和匹配的時間的總和。

常用演算法

暴力查詢

參考:

https://www.cnblogs.com/gaochundong/p/string_matching.html#naive_string_matching_algorithm

樸素的字串匹配演算法又稱為暴力匹配演算法(Brute Force Algorithm),它的主要特點是:

  • 沒有預處理階段;
  • 滑動視窗總是後移 1 位;
  • 對模式中的字元的比較順序不限定,可以從前到後,也可以從後到前;
  • 匹配階段需要 O((n - m + 1)m) 的時間複雜度;
  • 需要 2n 次的字元比較;

KMP 演算法

參考:

http://www.ruanyifeng.com/blog/2013/05/Knuth–Morris–Pratt_algorithm.html

詳細過程:

從左到右匹配,直到匹配到第一個字元相等,如下圖所示,然後繼續匹配後面的字元。

到了D,發現不對,這是如果暴力法,則直接將模式後移一位,重新匹配。KMP演算法的想法是,設法利用這個已知資訊,不要把"搜尋位置"移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。

在查詢的一開始根據模式字串,生成一張《部分匹配表》(Partial Match Table)

移動位數 = 已匹配的字元數 - 對應的部分匹配值

所以移動為數 = 6 - 2 =4

這個《部分匹配表》如何生成?

"部分匹配值"就是"字首"和"字尾"的最長的共有元素的長度。以"ABCDABD"為例,

- "A"的字首和字尾都為空集,共有元素的長度為0;

- "AB"的字首為[A],字尾為[B],共有元素的長度為0;

- "ABC"的字首為[A, AB],字尾為[BC, C],共有元素的長度0;

- "ABCD"的字首為[A, AB, ABC],字尾為[BCD, CD, D],共有元素的長度為0;

- "ABCDA"的字首為[A, AB, ABC, ABCD],字尾為[BCDA, CDA, DA, A],共有元素為"A",長度為1;

- "ABCDAB"的字首為[A, AB, ABC, ABCD, ABCDA],字尾為[BCDAB, CDAB, DAB, AB, B],共有元素為"AB",長度為2;

- "ABCDABD"的字首為[A, AB, ABC, ABCD, ABCDA, ABCDAB],字尾為[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度為0。

Python和Java實現參考自己的部落格:

https://blog.csdn.net/qqxx6661/article/details/79583707

Boyer-Moore

參考:

http://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html

幾種常見的字串匹配演算法的效能比較:

KMP演算法並不是效率最高的演算法,實際採用並不多。各種文字編輯器的"查詢"功能(Ctrl+F),大多采用Boyer-Moore演算法。

詳細過程:

首先,"字串"與"搜尋詞"頭部對齊,從尾部開始比較。我們看到,"S"與"E"不匹配。這時,"S"就被稱為"壞字元"(bad character),即不匹配的字元。我們還發現,"S"不包含在搜尋詞"EXAMPLE"之中,這意味著可以把搜尋詞直接移到"S"的後一位。

依然從尾部開始比較,發現"P"與"E"不匹配,所以"P"是"壞字元"。但是,"P"包含在搜尋詞"EXAMPLE"之中。所以,將搜尋詞後移兩位,兩個"P"對齊。

"壞字元規則":後移位數 = 壞字元的位置 - 搜尋詞中的上一次出現位置(如果"壞字元"不包含在搜尋詞之中,則上一次出現位置為 -1)

上圖中,比較的是P和E,出現在第6位(0開始),然後P上一次位置是4,所以6-4=2

接著繼續,一直比較到M:

根據"壞字元規則",此時搜尋詞應該後移 2 - (-1)= 3 位。問題是,此時有沒有更好的移法?

比較前面一位,"MPLE"與"MPLE"匹配。我們把這種情況稱為"好字尾"(good suffix),即所有尾部匹配的字串。注意,"MPLE"、"PLE"、"LE"、"E"都是好字尾

"好字尾規則":後移位數 = 好字尾的位置 - 搜尋詞中的上一次出現位置

這個規則有三個注意點:

(1)"好字尾"的位置以最後一個字元為準。假定"ABCDEF"的"EF"是好字尾,則它的位置以"F"為準,即5(從0開始計算)。

(2)如果"好字尾"在搜尋詞中只出現一次,則它的上一次出現位置為 -1。比如,"EF"在"ABCDEF"之中只出現一次,則它的上一次出現位置為-1(即未出現)。

(3)如果"好字尾"有多個,則除了最長的那個"好字尾",其他"好字尾"的上一次出現位置必須在頭部。比如,假定"BABCDAB"的"好字尾"是"DAB"、"AB"、"B",請問這時"好字尾"的上一次出現位置是什麼?回答是,此時採用的好字尾是"B",它的上一次出現位置是頭部,即第0位。這個規則也可以這樣表達:如果最長的那個"好字尾"只出現一次,則可以把搜尋詞改寫成如下形式進行位置計算"(DA)BABCDAB",即虛擬加入最前面的"DA"。

回到上文的這個例子。此時,所有的"好字尾"(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"還出現在頭部,所以後移 6 - 0 = 6位。

可以看到,"壞字元規則"只能移3位,"好字尾規則"可以移6位。所以,Boyer-Moore演算法的基本思想是,每次後移這兩個規則之中的較大值。

Boyer–Moore 演算法的精妙之處在於,其通過兩種啟示規則來計算後移位數,且其計算過程只與模式 P 有關,而與文字 T 無關。因此,在對模式 P 進行預處理時,可預先生成 "壞字元規則之向後位移表" 和 "好字尾規則之向後位移表",在具體匹配時僅需查表比較兩者中最大的位移即可。

Rabin-Karp

參考:

https://www.cnblogs.com/tanxing/p/6049179.html

首先計算模式字串的雜湊函式, 如果找到一個和模式字串雜湊值相同的子字串, 那麼繼續驗證兩者是否匹配.

這個過程等價於將模式儲存在一個散列表中, 然後在文字中的所有子字串查詢. 但不需要為散列表預留任何空間, 因為它只有一個元素.

基本思想

長度為M的字串對應著一個R進位制的M位數, 為了用一張大小為Q的散列表來儲存這種型別的鍵, 需要一個能夠將R進位制的M位數轉化為一個0到Q-1之間的int值雜湊函式, 這裡可以用除留取餘法.

舉個例子, 需要在文字 3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3 查詢模式 2 6 5 3 5, 這裡R=10, 取Q=997, 則雜湊值為

2 6 5 3 6 % 997 = 613

然後計算文字中所有長度為5的子字串並尋找匹配

3 1 4 1 5 % 997 = 508

1 4 1 5 9 % 997 = 201

......

2 6 5 3 6 % 997 = 613 (匹配)

計算雜湊函式

在實際中,對於5位的數值, 只需要使用int就可以完成所有需要的計算, 但是當模式長度太大時, 我們使用Horner方法計算模式字串的雜湊值

2 % 997 = 2

2 6 % 997 = (2*10 + 6) % 997 = 26

2 6 5 % 997 = (26*10 + 5) % 997 = 265

2 6 5 3 % 997 = (265*10 + 3) % 997 = 659

2 6 5 3 5 % 997 = (659*10 + 5) % 997 = 613

這裡關鍵的一點就是在於不需要儲存這些數的值, 只需儲存它們除以Q之後的餘數.

取餘操作的一個基本性質是如果每次算術操作之後都將結果除以Q並取餘, 這等價於在完成所有算術操作之後再將最後的結果除以Q並取餘.

演算法實現:

建構函式為模式字串計算了雜湊值patHash並在變數中儲存了R^(M-1) mod Q的值, hashSearch()計算了文字前M個字母的雜湊值並和模式字串的雜湊值比較, 如果沒有匹配, 文字指標繼續下移一位, 計算新的雜湊值再次比較,知道成功或結束.

Java程式碼:

https://www.cnblogs.com/tanxing/p/6049179.html

蒙特卡洛演算法和拉斯維加斯演算法區別:

總結

優點:

  • 暴力查詢演算法:實現簡單且在一般情況下工作良好(Java的String型別的indexOf()方法就是採用暴力子字串查詢演算法);
  • Knuth-Morris-Pratt演算法能夠保證線性級別的效能且不需要在正文中回退;
  • Boyer-Moore演算法的效能一般情況下都是亞線性級別;
  • Rabin-Karp演算法是線性級別;

缺點:

  • 暴力查詢演算法所需時間可能和NM成正比;
  • Knuth-Morris-Pratt演算法和Boyer-Moore演算法需要額外的記憶體空間;
  • Rabin-Karp演算法內迴圈很長(若干次算術運算,其他演算法都只需要比較字元);

關注我

我是蠻三刀把刀,後端開發。主要關注後端開發,資料安全,爬蟲等方向。微信:yangzd1102

Github:@qqxx6661

個人部落格:

原創部落格主要內容

  • Java知識點複習全手冊
  • Leetcode演算法題解析
  • 劍指offer演算法題解析
  • SpringCloud菜鳥入門實戰系列
  • SpringBoot菜鳥入門實戰系列
  • Python爬蟲相關技術文章
  • 後端開發相關技術文章

個人公眾號:後端技術漫談

如果文章對你有幫助,不妨收藏起來並轉發給您的朋友們~

我的部落格即將同步至騰訊雲+社群,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=3mmnmn9r2ewwg