算法之「字符串匹配算法」

前言

一說到兩個字符串匹配,咱們很天然就會想到用兩層循環來匹配,用這種方式就能夠實現一個字符串是否包含另外一個字符串了,這種算法咱們稱爲 BF算法。java

BF算法

BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是將目標串 S 的第一個字符與模式串 T 的第一個字符進行匹配,若相等,則繼續比較 S 的第二個字符和 T的第二個字符;若不相等,則比較 S 的第二個字符和 T 的第一個字符,依次比較下去,直到得出最後的匹配結果。git

BF算法實現github

public static int bruteforce(String s,String t){
    int length = s.length();//目標字符串的長度
    int plength = t.length();//模式串的長度

    //循環目標字符串
    for(int i=0;i<length-plength;i++){
        //循環模式串
        int j=0;
        while((j < plength) && (s.charAt(i+j) == t.charAt(j))){
            j++;
        }
        if(j == plength){
            return i;
        }
    }

    return -1;
}

BF算法是一種蠻力算法,沒有任何優化,就是用兩層循環的比較,當字符串比較大的時候,執行效率就很是低下,不適合比較很是大的字符串。算法

該算法最壞狀況下要進行 $M*(N-M)$ 次比較,時間複雜度爲 $O(M*N)$。所以,若是咱們使用暴力搜索一串'm'字符中的'n'個字符串,那麼咱們須要嘗試 n*m 次。微信

雖然暴力搜索很容易實現,而且若是解決方案存在它就必定可以找到,可是它的代價是和候選方案的數量成比例的,因爲這一點,在不少實際問題中,消耗的代價會隨着問題規模的增長而快速地增加。所以,當問題規模有限,或當存在可用於將候選解決方案的集合減小到可管理大小的針對特定問題的啓發式算法時,一般使用暴力搜索。另外,當實現方法的簡單度比運算效率更重要的時候,也會用到這種方法。優化

咱們看到暴力搜索算法雖然不須要預處理字符串,但效率比較低下,由於它須要作不少沒必要要的匹配,所以咱們須要更高效的算法。spa

AC自動機算法

Aho–Corasick算法是由 Alfred V. Aho 和 Margaret J.Corasick 發明的字符串搜索算法,用於在輸入的一串字符串中匹配預先構建好的 Trie 樹中的子串。它與普通字符串匹配的不一樣點在於同時與全部字典串進行匹配。算法均攤狀況下具備近似於線性的時間複雜度,約爲字符串的長度加全部匹配的數量。指針

假設 m 爲模式的長度, n 爲要搜索的字符串長度, k爲字母表長度。該算法因爲須要預先構建好變異的 Trie 樹,所以須要 $O(mk)$ 的預處理時間。但在正真搜索字符串時的時間複雜度爲 $O(n)$,大大的提升了字符串搜索時間。code

該算法主要依靠構造一個有限狀態機(相似於在一個 Trie 樹中添加失配指針)來實現。這些額外的失配指針容許在查找字符串失敗時進行回退(例如設 Trie 樹的單詞 cat 匹配失敗,可是在 Trie 樹中存在另外一個單詞 cart,失配指針就會指向前綴 ca ),轉向某前綴的其餘分支,免於重複匹配前綴,提升算法效率。遞歸

當一個字典串集合是已知的(例如一個計算機病毒庫), 就能夠以離線方式先將自動機求出並儲存以供往後使用,在這種狀況下,算法的時間複雜度爲輸入字符串長度和匹配數量之和,也是 AC自動機算法經常使用的狀況。

構造

1.一棵根據單詞或者內容構建 Trie 樹。這個也叫 goto表。

2.在一個單詞結束的地方,會有指向一個 Trie樹根或者其餘分支上和它相同的字符,這個就是回退指針,免於重複匹配前綴,提升算法效率。好比下圖中的單詞 his 結尾的 s 指向 she 的 s,就能夠在原有的基礎上繼續向下查找,減小前綴的匹配次數。這個也叫 fail表。

查找

先查找當前節點的「孩子節點」,若是沒有找到匹配,查找它的後綴節點的孩子,若是仍然沒有,接着查找後綴節點的對應 fail表的後綴節點的孩子, 如此循環, 直到根節點,若是到達根節點仍沒有找到匹配則結束。

當算法查找到一個節點,則輸出全部結束在當前位置的字典項。輸出步驟爲首先找到該節點的字典後綴,而後用遞歸的方式一直執行到節點沒有字典前綴爲止。同時,若是該節點爲一個字典節點,則輸出該節點自己。

實現

在 GitHub 上有個開源的實現,有興趣的同窗能夠細讀。https://github.com/robert-bor...

有沒有不須要提早作那麼多處理,後期匹配又快的算法呢?
答案就是 KMP算法。

KMP算法

它以三個發明者命名,起頭的那個 K 就是著名科學家 Donald Knuth。

KMP算法示例
這個算法的關鍵在於 部分匹配表(partial match table,也叫 PMT表),那這個部分匹配表怎麼來的呢?咱們看下模式串 「abababca」 的部分匹配表:

char:  | a | b | a | b | a | b | c | a |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 
value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |

對應的 value,就是模式串 「abababca」 的部分匹配表。

要理解部分匹配表,咱們須要先知道前綴和後綴。
"前綴"指除了最後一個字符之外,一個字符串的所有頭部組合;
"後綴"指除了第一個字符之外,一個字符串的所有尾部組合。

字符串:Knuth
前綴:  K, Kn, Knu, Knut
後綴:  nuth, uth, th, h

如今咱們根據"前綴"和"後綴"來求出模式串 「abababca」 的部分匹配表。

1. "a" 的前綴和後綴都爲空集,沒有共有元素,長度爲 0;
2. "ab" 的前綴爲[a],後綴爲[b],沒有共有元素,長度爲 0;
3. "aba" 的前綴爲[a, ab],後綴爲[ba, a],共有元素 "a", 長度爲 1;
4. "abab" 的前綴爲[a, ab, aba],後綴爲[bab, ab, b],共有元素 "ab", 長度爲 2;
...
7. "abababc" 的前綴爲[a, ab, aba, abab, ababa, ababab],後綴爲[bababc, ababc, babc, bc, c],沒有共有元素, 長度爲 0;
8. "abababca" 的前綴爲[a, ab, aba, abab, ababa, ababab, abababc],後綴爲[bababca, ababca, babca, bca, ca, a],共有元素 "a", 長度爲 1;

怎麼根據部分匹配表查找呢?

當咱們找到部分匹配時,咱們可使用部分匹配表中的值來跳過(而不是重作沒必要要的舊比較)。

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

咱們將 「abababca」 模式串與 「bacbababaabcbab」 字符串的匹配舉例。

bacbababaabcbab
 |
 abababca

第一步,因爲咱們的模式串開頭是 "a", 而後循環字符串,第二個字符就是 "a",這說明有一個字符匹配。因爲下一個字符不匹配,所以咱們須要移動。移動的位數等於 部分匹配表[ 已匹配的字符數 - 1 ],也就是部分匹配表中下表爲 0 的值,爲 0。所以咱們須要向後跳過 0 個字符,繼續循環。

bacbababaabcbab
    |||||
    abababca

如今匹配了 5 個字符,須要跳過的字符數就是 5 - 3 = 2,須要向後移動 2 位。

// x 表示跳過的字符

bacbababaabcbab
    xx|||
      abababca

如今匹配了 3 個字符,須要跳過的字符數就是 3 - 1 = 2,須要向後移動 2 位。

bacbababaabcbab
      xx|
        abababca

此時,咱們的模式比文本中的其他字符長,因此咱們知道並無匹配。

總結

BF算法 屬於暴力破解的方式,利用窮舉來實現字符串的查找,當字符串太長的時候,查找速度是很是慢的。

因而又有了 AC自動機算法,它是先構建一個相似 Trie樹 的結構,在根據 Trie樹 查找對應的字符串查,它的時間複雜度差很少是線性的,但須要提早構建好 Trie樹。

有沒有一種算法既不是窮舉的方式,也不須要提早構建好 Trie樹。

那就是大名鼎鼎的 KMP算法,它經過運用對這個詞在不匹配時自己就包含足夠的信息來肯定下一個匹配將在哪裏開始的,從而避免從新檢查先前匹配的字符,也就是咱們上面的部分匹配表(PMT表)。

PS:
清山綠水始於塵,博學多識貴於勤。
關注微信公衆號獲取最新文章。
微信公衆號:「清塵閒聊」。
歡迎一塊兒談天說地,聊代碼。

相關文章
相關標籤/搜索