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


在這里插入圖片描述

前言

新開專欄【數據結構拾遺】

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

參考

子字符串匹配

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

  • 文本長度: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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM