1. 程式人生 > >字串匹配自動機的演算法原理

字串匹配自動機的演算法原理

上一節,我們知道,如何構造一個有限狀態機,用於字串匹配,我們只給出了怎麼做,這一節,我們詳細說明一下,為什麼要這麼做,我們要從數學上驗證上一節我們給出的演算法邏輯是經得起考驗的。

這裡寫圖片描述

如上圖所示,有限狀態自動機有以下幾個特點:
1. 它由一系列的狀態節點組成,我們用Q來表示這些節點的集合
2. 狀態機一開始就會處於初始狀態,我們用q0來表示
3. 所以狀態中,必有一個狀態A Q 叫接收狀態,例如上圖的節點1.
4. 組成字串的字符集, 例如上圖中,字符集只包含a,b兩個字元。
5. 當狀態機處於某個狀態,接收到一個輸入字元時,會跳轉到另一個狀態,這種跳轉我們用一個函式δ 來表示,例如,根據上圖,當狀態機處於狀態0,輸入是字元a時,狀態轉移到狀態1,於是就有

(0, a) = 1

我們再引入一個函式ϕ, 叫最終狀態函式,它接收一個字串,然後給出狀態機讀入該字串後,最終會處於哪個狀態,例如給定字串”abba”,上面的狀態機接收後,最終會處於狀態1,於是就有 ϕ(“abba”) = 1, 如果給定的字串是”aabb”, 狀態機接收該字串後,最終處於的狀態是0,所以 ϕ(“aabb”) = 0

狀態機一開始時,處於初始狀態,也就是狀態機什麼都不接收時或接收空字串時就處於初始狀態,於是我們有:
ϕ(ϵ) = q0
假設w 是一個字串,那麼有:
ϕ(wa) = δ(ϕ(w), a)
上面這個公式需要好好解釋一下,假定w=”aabb”, wa = “aabb” + ‘a’ = “aabba”.

當狀態機接收字串”aabb”後,處於狀態0,於是就有 ϕ(“aabb”) = 0.狀態機接收字串wa後所達到的最終狀態,相當於先接收字串w,達到狀態0後,再接收最後的字元a,使得狀態機從狀態0,再進行一次跳轉,根據上圖,狀態機處於狀態0,輸入字元為a時,跳轉到狀態1,於是有δ(0, a) = 1, 又由於狀態0是狀態機接收字串”aabb”後的最終狀態,所以0 = ϕ(“aabb”), 代入上一個式子有δ( ϕ(“aabb”), a) = 1, 先接收字串”aabb”,到達一個狀態,然後再接收字元a到達另一個狀態,這不就相當於字串”aabba” = “aabb” + ‘a’, 所抵達的最終狀態嗎,所以就有:
ϕ

(“aabba”) = δ(ϕ(“aabb”), a).

數學推理是一個比較燒腦的過程,想必上面的解釋會讓不少同學抓耳撓腮一陣子。

假設要查詢的字串,我們用P來表示,對於給定一個文字T, 如果P 的前k個字元所組成的字串是T的字尾的話,我們就定義:
σ(T) = k,

例如 P=”abcdefg”, T = “hhhhhhhha”, 那麼P的前1個字元所組成的字串”a”是T的字尾,所以 σ(T) = 1

T=”hhhhhhab”, 那麼P的前兩個字元組成的字串”ab”構成T的字尾,於是σ(T) = 2.

T=”hhhhhabc”, P的前3個字元組成的字串”abc”構成T的字尾,於是有σ(T) = 3

依次類推。

上一節我們構造的狀態機是滿足以下條件的:
1. 如果P 含有m個字元,那麼狀態機就有m+1個狀態節點,他們分別為{0,1,2…m}, 並且初始狀態q0 = 0, 接收狀態是m.
2. 當狀態機處於狀態q時,如果接收字元a, 那麼狀態機要跳轉的下一個狀態是:
δ(q, a) = σ(Pqa)

上一節我們給出的程式碼有這麼一段:

 private void makeJumpTable() {
       int m = P.length();
       for (int q = 0; q <= m; q++) {
           for (int k = 0; k < alphaSize; k++) {

               char c = (char)('a' + k);
               String Pq = P.substring(0, q) + c;


               int nextState = findSuffix(Pq);
               System.out.println("from state " + q + " receive input char " + c + " jump to state " + nextState);
               HashMap<Character, Integer> map = jumpTable.get(q);
               if (map == null) {
                   map = new HashMap<Character, Integer>();
               }

               map.put(c, nextState);
               jumpTable.put(q, map);
           }
       }

String Pq = P.substring(0, q) + c; 這一句程式碼的作用,其實就是構造字串Pqa, 程式碼findSuffix(Pq); 其實就是計算σ(Pqa).

如果我們能夠證明,我們前一節構造的狀態機滿足:
ϕ(Ti) = σ(Ti)

ϕ(Ti) 表示把文字T前i個字元構造的字串輸入到狀態機後所達到的最終狀態。

σ(Ti) 表示,從P的前k個字元可以組成字串Ti的字尾,如果有某個i使得 ϕ(Ti) = m, 那麼根據上面式子,字串P將成為字串Ti的字尾,而Ti又是字串T的字首,這不就意味著字串P包含在T中了嗎,所以,如果我們構造的狀態機滿足上面的等式,那麼我們依次將T的字元輸入到狀態機中,當狀態機跳轉到狀態m時,就表明字串P包含在文字T中了。於是接下來我們將思考如何證明等式:

ϕ(Ti) = σ(Ti)

定理1:
對給定的匹配字串P, 以及文字x, 還有任意字元a, 我們有:
σ(xa) <= σ(x) + 1

令r = σ(xa), 如果r = 0 ,上面的等式明顯是成立的。因為σ(x)肯定是大於等於0的。如果r > 0, 也就是從P的第一個字元開始,連續r個字元所構成的字串Pr能構成xa的字尾,xa = x + ‘a’,也就是字串xa是以字元a結尾的, 那麼Pr的最後一個字元也肯定是’a’, 如果我們同時將字元’a’從Pr和xa的末尾去掉,那麼我們有Pr1是x的字尾,例如P=”bac”, x=”dddb”, 那麼P2 = “ba” 是xa(“dddba”) 的字尾,去掉末尾的字元a後,P1=”b” 仍然是字串x(“dddb”)的字尾。

由於Pr1 是x的字尾,而k=σ(x),表示最大的k,使得Pk是x的字尾,那麼就有 r-1 <= k 也就是 r-1 <= σ(x), 調整一下就有 r <= σ(x) + 1, 由於r = σ(xa), 於是就有:
σ(xa) <= σ(x) + 1

定理2:
對匹配字串P, 文字字串x, 以及任意一個字元a, 如果 q = σ(x), 那麼
σ(xa) = σ(Pqa)

先看個具體例項,P=”bacdb”, x=”ffffb”, 1 = q = σ(x), P1=”b”,
xa = “ffffba”, 於是2 = q = σ(xa), 而P1a = “ba”, 從字串P第一個字元開始,連續2個字元所構成的字串是P1a的字尾,於是有σ(