Algorithm - String matching
給兩個字串 T 和 P,找出 T 當中是否有一段字串正好是 P,並且找出其位置
字串搜尋當中,通常將兩字串的象徵符號取做 T 和 P
- T 意指 Text
- P 意指 Pattern
可以想作是從長篇文字 T 之中搜索小段文字 P
- 若 P 在平移 s 個單位後(T [s+1 … s+m] = P [1 … m],0 ≦ s ≦ n-m)
- 可以在 T 中該片段被找到
- 稱為「合法平移」(Valid shift)
- 無法在 T 中該片段被找到
- 稱為「非法平移」(Invalid shift)
- 可以在 T 中該片段被找到
Algorithm | Preprocessing time | Matching time |
---|---|---|
Native | 0 | O( (∣T∣-∣P∣+1) ×∣P∣) |
Rabin-Karp | θ(∣P∣) | O( (∣T∣-∣P∣+1) ×∣P∣) |
Morris-Pratt Automaton | O(∣P∣×∣Σ∣) | θ(∣T∣) |
Knuth-Morris-Pratt | θ(∣P∣) | θ(∣T∣) |
Naive string matching(窮舉法)
最直覺的算法
- 挪動 P 以對準 T 的各個位置
- 逐一比對字元、判斷是否相等
Example
- Algorithm
1 | void Nativestrinmatcher(string T,string P) { |
- 時間複雜度
- O( (|T|-|P|+1)×|P| )
- O( |T||P| )
- O( (|T|-|P|+1)×|P| )
- 空間複雜度
- O(1)
Rabin-Karp 演算法
由 Michael O. Rabin 及 Richard M. Karp 在 1987 年發展利用雜湊作字串判斷,帶有數學味道的演算法
- 一個長度為 m 的模板字串 P
- 視為一個 d 進制(d = ∣Σ∣)的數字 p
- 令 \(t_s\) 為 T[s+1…s+m] 轉換後的結果
- 問題轉換成「是否存在一個 k(0 ≤ k ≤ n − m),使得 \(p = t_k\)」
p = ∣Σ∣(P[m-1]+(∣Σ∣(P[m-2]+… (∣Σ∣(P[2]+∣Σ∣P[1]) …))) + P[m]
\(\Rightarrow p = |Σ|^{m-1}P[1]+|Σ|^{m-2}P[2]+\ldots+|Σ|^{0}P[m]\)
\(t_{s+1} = |Σ|(t_s - |Σ|^{m-1}T[s+1])+T[s+m+1]\)
Example
Σ = {0, 1, …, 9},d = ∣Σ∣ = 10
- Pattern P[1…m]
- p 為對應的十進位數字
- Text T[1…n]
- \(t_s\) 為長度為 m 子字串(T[s+1…s+m],s = 0, 1, …, n-m)中對應的十進位數字
- 字串 [3 1 4 1 5 2]
- 轉換後為 314,152
\(t_{s+1}\) 與 \(t_s\) 的關係
\(t_{s+1} = 10(t_s - 10^{m-1}T[s+1])+T[s+m+1]\)
所以,上述表示為
T = [3 1 4 1 5 2]、m = 5、d = 10
\(t_s = t_0 = 31,415\)
\(\Rightarrow t_{s+1} = t_1 = 10(31,415-10^{5-1}\cdot3)+2 = 14,152\)
- 因為 p 以及 \(t_k\) 可能非常大
- 因此比較時間不能視為常數
- 通常將其 mod 一個大質數 q
- 因為如此當 \(p = t_k\) 時,不一定匹配成功,須再作進一步驗證
- Spurious hit(假性命中)
- \(p = t_k\) 但 P[1…m] ≠ T[s+1…s+m]
- Valid hit(完全命中)
- \(p = t_k\) 與 P[1…m] = T[s+1…s+m]
Example
- P[1…5] = 31,415
- p = 31,415 mod 13 = 7
- Algorithm
1 | void Rabinkarpmatcher (T,P,d,q) { |
- 時間複雜度
- 預處理
- θ (∣P∣)
- 比對程序
- O( (∣T∣-∣P∣+1)×∣P∣ )
- 發生在「Worst case」情況
- 在多數比對次數少、q 大於 m 的情況為線性複雜度
- 預處理
- 空間複雜度
- O(1)
Knuth-Morris-Pratt 演算法
由 Donald Knuth、Vaughan Pratt、J. H. Morris 三人於西元 1977 年共同聯合發表,最差情況為 O(n) 的字串匹配演算法
觀察暴力演算法
- 存在不必要的工作
- 從左往右一一比對字元,一旦發現字元不同,將 P 往右挪動一位
- 往右挪動 P 之前,當下比對成功的字串片段,可以不必花時間在上面
Example
- T = [aabzabzabcz]
- P = [abzabc]
(從左往右一一比對字元,一旦發現字元不同,將 P 往右挪動一位)
(在往右挪動 P 之前,當下比對成功的字串片段「abzab」可以加以利用)
(繼續往右挪動 P,挪動一個位置、挪動兩個位置、…)
觀察上述行為
- 挪動一個位置
- 比較『abzab 的後四個字元』與『abzab 的前四個字元』
- 挪動兩個位置
- 比較『abzab 的後三個字元』與『abzab 的前三個字元』
- 因此若預先知道『 abzab 之「次長相同前綴後綴」是 ab』
- 可大幅挪動 P
- 從「V」處繼續向右一一比對字元
- 每當比對失敗,就從當前比對成功的字串片段,取其「次長的相同前綴後綴」大幅挪動 P
「相同前綴後綴」(Prefix-suffix)
「次長相同前綴後綴」
- 一個字串的「最長相同前綴後綴」為原字串
- 「最短相同前綴後綴」為空字串
- 「次長相同前綴後綴」就是第二長的「相同前綴後綴」
窮舉法的過程當中,當前比對成功的字串片段是 P 的前綴
- 因為無法預測是 P 的哪個前綴
- 所以預先計算 P 每個前綴的「次長的相同前綴後綴」
- 衍生出了「Failure function」
步驟
- 預先計算 P 的每種前綴的「次長相同前綴後綴」
- 意旨算出 P 的「Failure function」
- 從左往右依序比對字元
- 比對成功時
- 繼續比對下個字元
- 比對失敗時
- 從比對成功的字串片段取其「次長的相同前綴後綴」以大幅挪動 P
- 當全部比對成功搜尋到 P 時
- 取 P「次長的相同前綴後綴」以大幅挪動 P
- 比對成功時
- Algorithm
1 | // pattern[0..m] |
時間複雜度分析(均攤分析)
以「Multipop stack」概念作均攤分析,以字元兩兩比對總次數作為時間複雜度
(1)進行字串搜尋的過程中
- 「Stack」S 的元素
- 當下比對成功的字串片段 S
- 一開始 S 長度是零
- 當下比對成功的字串片段 S
- 若字元比對成功
- S 增加一字元,視為「Push stack」
- 若字元比對失敗
- 大幅挪動 P,S 只剩下「次長的相同前綴後綴」,視為「Multipop」
- 實際上 S 瞬間大幅變短只需要 O(1) ,時間複雜度遠比「Multipop」小
最多有 T 個字元放入 S(S 增加一字元)
最多有 T 個字元彈出 S(大幅挪動 P,S 只剩下「次長的相同前綴後綴」)
\(\Rightarrow\)字元兩兩比對的總次數不超過 2T 次
(2)計算 P 的「Failure function」過程中
原理相同,字元兩兩比對的總次數不超過 2P 次
- 總時間複雜度
- O(∣T∣+∣P∣)
Failure function
在比對失敗時會使用之
因為函數的定義域是 Prefix,又稱作 Prefix function
因為此函數的值域是 Border,又稱作 Border function
- 字串函數
- 輸入字串的其中一個前綴,輸出該前綴的「次長的相同前綴後綴」
- 計算「Failure function」
- Dynamic Programming
- 分割問題
- P[0...i] 除去尾端字元 P[i]
- 利用已知 P[0...i-1] 的「次長相同前綴後綴」
- 得到 P[0...i] 的「次長相同前綴後綴」
F[k]:P[0...k] 之「次長的相同前綴後綴」長度
(1)將 F[0] 初始化為 -1
- 長度為 1 的子字串,不存在「次長相同前綴後綴」
(2)F[k]:探討 P[1...k-1] 與 P[k] 之間的關係
- P[ F[k-1]+1 ] == P[k]
- 意旨「第 k 個字元」與「P[1...k-1] 之『次長的相同前綴後綴』下一個字元」相等
- \(\Rightarrow\) F[k] = F[k-1]+1
對「P[1...k-1] 之『次長的相同前綴後綴』」作探討
- P[ F[k-1]+1 ] ≠ P[k]
- 「第 k 個字元」與「P[1...k-1] 之『次長的相同前綴後綴』下一個字元」相異
若存在 P[ F...[F[k-1]]+1 ] == P[k],則 F[k] = F...[F[k-1]]+1
若不存在 P[ F...[F[k-1]]+1 ] == P[k],則 F[k] = -1
上述可以表達為:
\(f[k]\left\{\begin{matrix} -1 & if \;k = 0\\ f^m[k-1] +1 & 最小的整數\; m\;使得\; P[f^m[k-1]+1] == P[k] \\ -1 & 不存在整數\; m \;可以使得 P[f^m[k-1]+1] == P[k]\end{matrix}\right.\)
Algorithm
1 | // pattern[0..m] |
Morris-Pratt Automaton
此演算法可以化作自動機,轉化的時間複雜度為 O( ∣P∣×∣Σ∣ )
- Σ 為字元集合
化作自動機之後,字串搜尋的過程就變得更簡單了,甚至可以設計成電子迴路
轉化的原理,是針對每個狀態,都找出經由「Failure function」能到達的狀態們,然後建立轉移邊,連到那些狀態們的下一個狀態