32.字符串匹配

研究問題:

在文本T中找到某個模式P所出現的位置。
定義文本是一個長為 n 的數組T[0..n-1],而模式P是一個長為 m 的數組P[0..m- 1],m≤n
并且若T[s..s+(m-1)]=P[0..(m-1)]則稱模式P在T中出現且偏移為 s (P在文本T中的位置 是從第 s+1 個開始的)

常見算法的時間:

算 法 預處理時間 匹配時間
樸素算法 0 O((n-m+1)m)
Rabin-Karp \Theta(m) O((n-m+1)m)
有限自動機算法 O(m|\Sigma|) \Theta(n)
Knuth-Morris-Pratt \Theta(m) \Theta(n)

樸素算法

對 n-m+1 個可能的 s 值做檢測,看是否滿足T[s..s+(m-1)]=P[0..(m-1)]

  • 實現代碼:
int naiveStringMatcher(string T, string P){
    int n = T.length();
    int m = P.length();
    for (int s = 0; s < n - m; s++){
        int i;
        for (i = 0; i < m; i++){
            if (T[s + i] != P[i])
            break;
        }
        if (i == m) return s; //返回下標s
    }
    return -1; //這里找不到合適的值就返回-1
}

最壞情況下,運行時間為 O((n-m+1)m) 如果 m=floor(n/2.) 則運行時間為\Theta(n^2)

Rabin-Karp 算法

Rabin-Karp 算法的預處理時間是\Theta(m),最壞情況下運行時間是O((n-m+1)m),但是平均運行時間比較快。

對于每個字符串,可以用長度為 k 的十進制數來表示由 k 個連續的字符組成的字符串,例如字符串31415可以對應十進制數31415。給定一個模式 P[0..(m-1)],假設 p 表示其十進制值,對應的文本 T[0..(n-1)]中, 假設t_s表示長度為 m 的子字符串 T[s..(s+m-1)] 的十進制值,當且僅當 P[0..(m-1)]=T[s..(s+m-1)] 時,有 p=t_s。如果能在時間 \Theta(m) 內計算出 p 值,并在總時間 O(n-m+1) 內計算出所有 t_s 值,那么通過比較所 p 和所有 t_s 值就可以在 \Theta(n) 時間內計算出所有偏移 s 。

p 可以運用霍納法則在 \Theta(m) 時間內計算得到:
p = P[m]+10(P[m-1]+10(P[m-2]+...+10(P[2]+10P[1])))

t_s可以迭代得到,因為:
t_{s+1} = 10(t_s-10^{m-1}T[s+1])+T[s+m+1]
每次去掉高位數字,然后乘以10,再加上低位數字。

到目前為止的問題是 p 和 t_s 的值可能過大,因此可以選取一個合適的模 q 來計算 p 和 t_s 的模,我們可以在 \Theta(m) 時間內計算出模 q 的 p 值,并且可以在 \Theta(n-m+1) 時間內計算出模 q 的所有 t_s 值。

  • 遞推式:

t_{s+1} = (d(t_s-T[s+1]h)+T[s+m+1])\ mod\ q
d為字母表{0, 1, ..., d-1}的進制,t_s \equiv d^{m-1}\ (mod\ q) 是一個具有 m 數位的文本窗口的高位數位上的數字“1”的值。

但是, t_s \equiv p\ (mod\ q) 并不能說明 t_s = p, 反之若 t_s \neq p\ (mod\ q), 則一定有 t_s \neq p 。求余的結果可以用于快速檢測無效偏移 s, 但是對于有效偏移,還需要重新對該偏移逐個檢測,否則就是一個偽命中點

  • 實現代碼:
int mod(int a, int b){ //求余運算
    return (a % b >= 0) ? (a % b) : (a % b + b);
}

void RabinKarpMatcher(string T, string P, int d, int q){
    int n = T.length();
    int m = P.length();
    int h = int(pow(d, m - 1)) % q;
    int p = 0;
    vector<int> t(n - m + 1, 0);
    for (int i = 0; i < m; i++){   // processing 
        p = mod((d * p + P[i]), q);
        t[0] = mod((d * t[0] + T[i]), q);
    }
    for (int s = 0; s <= n - m; s++){  // matching
        if (p == t[s]){
            if (P == T.substr(s, m))
                cout << "s: " << s << endl;
            else
                cout << "error match!" << endl;
        }
        if (s < n - m)
            t[s + 1] = mod((d * (t[s] - T[s] * h) + T[s + m]), q);
    }
}

如果模 q=11, 那么當 Rabin-Karp 算法在文本 T = 3141592653589793 中尋找模式 P = 26 時,會遇到 3 個偽命中點。即 RabinKarpMatcher(T, P, 10, 11) 時,會遇到3個 error match!

有限自動機算法

有限自動機是一個處理信息的簡單機器,通過對文本字符串 T 進行掃描,找出模式 P 的所有出現位置。這些字符串匹配的自動機只對文本字符檢查一次,并且檢查每個字符的時間為常數,因此模式預處理和建立自動機的時間為 \Theta(n) 。但是如果字符集\Sigma很大的話,建立自動機的時間也較多。

  • 有限自動機的定義:

一個有限自動機是5個類型的元組: (Q,\ q_0,\ A,\ \Sigma,\ \delta)
Q狀態的有限集合
q_0\in Q初始狀態
A\subseteq Q是一個特殊的接受狀態集合
\Sigma是有限輸入字母表
\delta是一個從Q \times \SigmaQ的函數,稱為M的轉移函數

為了便于說明給定模式 P[0...(m-1)] 的字符串匹配自動機,定義一個輔助函數 \sigma, 稱為對應 P 的后綴函數。其中 \sigma(x)x 的后綴 P 的最長前綴的長度。
\sigma(x) = max\{k:\ P_k\sqsupset x\}
對于模式 P=ab, 有 \sigma(\varepsilon)=0\sigma(ccaca)=1, \sigma(ccab)=2
給定模式 P[0..(m-1)], 相應的字符串匹配自動機定義如下:
1)狀態集合Q為{0, 1, ..., m}。開始狀態q_0是0狀態,并且只有狀態m是唯一被接受的狀態。
2)對任意的狀態q和字符a,轉移函數\delta定義如下:
\delta(q,a)=\sigma(P_qa)

  • 一個自動機的例子:

輸入模式 P = ababaca,長度為7個字符,因此有狀態0, 1, ..., 7,假設字母表為{a, b, c}
則有:
\delta(0,a)=\sigma(P_0a)=\sigma(a)=1
\delta(0,b)=\sigma(P_0b)=\sigma(b)=0
\delta(0,c)=\sigma(P_0c)=\sigma(c)=0
\delta(1,a)=\sigma(P_1a)=\sigma(aa)=1
\delta(1,b)=\sigma(P_1a)=\sigma(ab)=2
\delta(1,c)=\sigma(P_1a)=\sigma(ac)=0
...
\delta(6,a)=\sigma(P_6a)=\sigma(ababaca)=7

因此可以有如下字符串匹配的狀態轉換圖:

狀態 a b c
0 1 0 0
1 1 2 0
2 3 0 0
3 1 4 0
4 5 0 0
5 1 4 6
6 7 0 0
7 1 2 0

其中狀態7是僅有的接受狀態

  • 實現代碼:
vector<vector<int>> computeTransFunc(string P, int len){  //預處理,計算delta
    int m = P.size();
    vector<int> temp(len, 0);
    vector<vector<int>> delta(m + 1, temp);
    for (int q = 0; q <= m; q++){
        int k;
        for (int a = 0; a < len; a++){  // 遍歷字母表,這里是數字0到(len-1),如果是小寫字母,可以通過-'a'操作得到對應的0到25
            string Pqa = P.substr(0, q) + to_string(a);
            k = (m + 1 <= q + 2) ? (m + 1) : (q + 2);
            string Pqasub = Pqa;  
            //這里借助一個Paqsub來存儲Pqa串的長度為k的后綴,因為k可能大于Pqa.size(),直接調用Pqa.substr(Pqa.size()-k)會報錯
            do {
                k--;
                int lenPqa = Pqa.size() - k;
                Pqasub = (lenPqa >= 0) ? Pqa.substr(lenPqa) : Pqa;
            } while (P.substr(0, k) != Pqasub);  // k--直到P的k前綴是Pqa的后綴為止,循環必然會停止,因為空串是任何字符串的后綴
            delta[q][a] = k;
        }
    }
    return delta;
}
void finiteAutomationMatcher(string T, vector<vector<int>> delta, int m){  //匹配過程
    //m是唯一接受狀態,例如上面例子中的7
    int n = T.size();
    int q = 0;
    for (int i = 0; i < n; i++){
        q = delta[q][T[i] - '0'];
        if (q == m)
            cout << "Pattern occurs with shift" << i + 1 - m << endl;
    }
}
int main(){
    string T("0201010102010");
    string P("0101020");
    vector<vector<int>> delta = computeTransFunc(P, 3);
    finiteAutomationMatcher(T, delta, 7);
    return 1;
}

本算法需要O(m|\Sigma|)的預處理時間以及\Theta(n)的匹配時間。

Knuth-Morris-Pratt算法

本算法無需計算\delta,匹配時間也同樣是\Theta(n),只需要用到一個輔助函數\pi,它在\Theta(m)時間內根據模式預先計算出來,并存儲在數組\pi[0..(m-1)]中。
模式的前綴函數pi包含模式其自身的偏移進行匹配的信息。這些信息可以用于在樸素的字符串匹配算法中避免對無用偏移進行檢測,也可以避免在字符串匹配自動機中,對整個轉移函數\delta的預先計算。如果q個字符已經匹配成功,那么可以根據這q個已知的字符,我們能夠立即確定某些偏移是無效的。

  • 一個例子:

對于模式P=ababaca,目前已經在T中匹配到了ababa,q=5個字符已經匹配成功,同時發現T中的下一位不匹配。根據5個匹配字符的有用信息,這里我們發現P_3(aba)是P(ababaca)的最長前綴的同時,也是P_5(ababa)的一個真后綴,即\pi[5]=3。在偏移s有q個字符成功匹配,則下一個可能有效的偏移為s'=s+(q-\pi[q])

  • 函數定義:

已知一個模式P[0..(m-1)],模式P的前綴函數是函數\pi:\ \{0,1,..,m-1\}\rightarrow\{0,1,..,m-1\},滿足
\pi[q]=max\{k:k<q\ and\ P_k \sqsupset P_q \}
\pi[q]P_q的真后綴P的最長前綴長度。

  • 具體程序實現:
vector<int> computePrefixFunc(string P){
    int m = P.size();
    vector<int> pi(m, 0);
    pi[0] = -1;
    int k = -1;
    for (int q = 1; q <= m - 1; q++){
        while (k > -1 && P[k + 1] != P[q])
            k = pi[k];
        if (P[k + 1] == P[q])
            k++;
        pi[q] = k;
    }
    return pi;
}
void kmpMatcher(string T, string P){
    int m = P.size();
    int n = T.size();
    vector<int> pi = computePrefixFunc(P);
    int k = -1;
    for (int i = 0; i < n; i++){
        while (k >-1 && P[k + 1] != T[i])//ptr和str不匹配,且k>-1(表示P和T有部分匹配)
            k = pi[k];//往前回溯
        if (P[k + 1] == T[i])
            k = k + 1;
        if (k == m - 1){ //說明k移動到ptr的最末端
            cout << i - m + 1 << endl;//返回相應的位置
        }
    }
}
int main()
{
    string T("0201010102010");
    string P("0101020");
    kmpMatcher(T, P);
}

該算法的預處理時間減少為\Theta(m),匹配時間為\Theta(n)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • "use strict";function _classCallCheck(e,t){if(!(e instanc...
    久些閱讀 2,046評論 0 2
  • 秋天本是個收獲的季節,我卻獨自神傷。我本來是個大學生,現在卻在干修路的工作,一天天的在工地上耗著,有家也回不了,...
    雪魂1閱讀 314評論 1 0
  • 我是不相信感情的。那種幾年前我們親如一家,幾年間我們各自被生活吊打,幾年以后我們再見了,依然可以你死我活的所謂"知...
    貝龍閱讀 426評論 12 7