串匹配算法


前言

串匹配問題是解決許多應用(文本編輯器,數據庫檢索,C++模板匹配,模式識別等等)的重要技術。

這個問題有兩個輸入,第一個是文本(Text),第二個是模式(Pattern),目的是要在文本中尋找模式。通常而言文本要遠大於模式。

T : now is the time for all good people to come (長度為n)

P :people (長度為m)

串匹配問題可分為四種類型:

  • detection : P是否出現?
  • location : P首次出現在哪里?
  • counting : P出現了多少次?
  • enumeration : 各出現在哪里?

顯然,解決location是最重要的,如果監測到了,就表明出現了(detection),出現多少次,只要將未比較的字符串根據同樣的方法求得下一次首次出現的位置,直到整個文本結束,出現在哪里只要記錄位置做標記即可。

下面開始介紹串匹配算法。

暴力匹配

思想是自左而右,以字符為單位,依次移動模式串,直到某個位置發生匹配。

1570533478385

這個算法最好的情況是第一次就比對成功,最好情況的上邊界則是每次比對時,第一個字符都不匹配,這樣就移動一格,最好情況的復雜度就等於\(\Omega(n)\), n為文本的長度。最壞的情況是每次比較模式最后一個字符的時候才發現不匹配,這樣就會導致最壞情況,時間復雜度為\(\mathcal{O}(n \cdot m)\).

C++實現版本1:

int match(string P, string T) {
	size_t n = T.size(), i = 0;
	size_t m = P.size(), j = 0;
	while (i < n - m + 1 && j < m)     //自左向右逐次比較
		if ( T[i] == P[j]) { i++; j++;}  // 若匹配,則轉到下一對字符
		else               { i -= j - 1; j = 0;}  // 否則,T回退,P復位
	return i - j;
}

C++實現版本2:

int match(string P, string T) {
	size_t n = T.size(), i;
	size_t m = P.size(), j;
	for ( i = 0; i < n - m + 1; i++) {  //T[i]與P[0]對齊
		for ( j = 0; j < m; j++)        //逐次匹配
			if ( T[i+j] != P[j]) break; //失配則轉到下一位置
		if ( m <= j) break;             //匹配成功,退出,返回i
	}
	return i;
}

兩個實現版本的返回值都是位置信息,當i等於n - m + 1的時候說明未找到模式,否則就是找到了。

KMP :模式記憶

暴力匹配算法存在着冗余的問題,當最壞情況時,最后一個字符匹配失敗,模式串和文本串的指針都要發生回退。

KMP算法的原理是利用Pattern構建一個查詢表,根據查詢表進行來指導移動位數,並且文本的索引不需要回退。理解這種算法我推薦阮一峰老師的KMP博客(真心推薦看看),講得非常清晰,非常直觀。

假設你看過阮老師的博客知道原理了,現在來看next表的構建代碼:

vector<int> buildNext(string P) { //構造模式串P的next表
    size_t m = P.size(), j = 0;   //“主”串指針
    vector<int> N(m, 0);          //next表
    int t = N[0] = -1;			  //模式串指針(通配符*)
    while ( j  < m - 1 )          //j是不會減小的,j會在循環內變為m-1,此時退出
        if ( 0 > t || P[j] == P[t] ) { //當出現通配符也就是0>t, 當前j自加1,next表對應j為0。
            						   //當不是通配符時,比較是否相等,相等則next表對應j自加1
            j++; t++;
            N[j] = t;
        }
    	else
            t = N[t];  //失配,根據前面得到的next,來看應該從那里開始比較,比如下面的匹配等於4的時候,e不等於c,查表知e所在的位置為0,也就是沒有相同的前后綴,所以從0開始繼續匹配,如果大於0,說明有共同前后綴,此時應該不從0開始,因為有共同前后綴,可以避開節省時間。
    return N;
}

這里需要注意的一點是,阮一峰老師的博客中當前next表是代表當前j的公共最大前后綴的長度,而這個實現中當前next表是代表j-1的公共最大前后綴的長度。

關於t = N[t]可以見下圖,當X不匹配Y的時候,此時我們根據next表,由當前next表的值知,P[0, t)和P[j - t, j)是相同的,此時應該移動j-t,也就是從第t位開始比較,也就是N(t)的長度。有一種特殊情況需要考慮,當N(t)等於0時,此時從0開始比較,如果第0位也不等於當前j,根據性質,t此時就等於-1了,此時就進入0>t的條件,自增j,自增t,當前j沒有共同前后綴。這里開始設N[0]等於-1以及t等於-1,有兩層作用,第一層是為了首輪比較時,需要隔開一位比較。第二層作用是為了防止后面與第一位不相等時,可以根據-1這個條件進入if條件,防止卡死。很是巧妙。

1570590461519

下面有一個事例:

1570542610277

1570543288650

有了next表的構造方法,接下來就是根據next表進行匹配了。匹配代碼如下:

int match(string P, string T) {
    vector<int> next = buildNext(P);
    size_t n = T.size(), i = 0;
    size_t m = P.size(), j = 0;
    while (j < m && i < n)
        if (0 > j || T[i] == P[j]) { i++; j++;}
    	else j = next[j];
    return i - j;
}

理解了next表的構造原理,其實就理解了匹配過程,next構造過程就是模式串的自我匹配。當失配時,如果next表的值大於0,說明有公共的前后綴,那么就不需要從0開始比較,直接從公共前后綴的后一個字符與當前文本的第j個字符開始比較。

KMP再改進

考慮下面這個情況,明知T[4]不等於P[4]且P[1] = P[2] = P[3] = P[4],還要比對剩余的P[1], P[2], P[3], 這是沒有必要的,這是需要改進next表。

1570591941387

改進只需要把next中的N[j] = t換成N[j] = ( P[++j] != P[++t] ? t : N[t] )即可。如下所示:

1570592311427

因為相同,所以可以直接跳過他們,更快。

KMP算法的時間復雜度是\(O(m + n)\), 空間復雜度是\(O(m+n)\). 匹配過程令k = 2i- j,k每次循環至少加1,判斷為真則i加1,判斷為假,j至少減1,所以k <= 2n - 1; 同理next過程也是如此。

KMP小結:

  • 以判斷公共前后綴來進行模式串的移動,有公共前后綴,移動到前綴的下一位即可,沒有公共前后綴則移動到頭部。
  • 通過通配符來有效構造next表,表的第一位為-1,當第一位對齊不相等的時候,這時通配符匹配,使文本串(也包括模式串的自我匹配)可以移動起來,不至於卡死。
  • 當發生重復串的時候,跳過他們,不進行比較。

BM算法

對於BM算法的介紹,我同樣推薦看阮一峰老師的BM博客(真心推薦看看),講的十分清楚。同樣假設你看過博客知道原理了,就知道BM算法有兩個next表,一個是壞字符(bad character)bc表,另一個是好后綴(good suffix)gs表,現在來看看如何構造這兩個表。

bc表

對於壞字符表,構造起來很簡單,它是記錄模式串中每種字符最后出現的位置,代碼如下:

vector<int> buildBC(string P){
    vector<int> bc(256, -1);
    for(size_t m = P.size(), j = 0; j < m; j++)
        bc[ P[j] ] = j;
    return bc;
}

壞字符移動規則: 后移位數 = 壞字符的位置- 搜索詞中的上一次出現位置

基於BM-DC的算法最好情況就是\(O(n/m)\), 最壞情況是\(O(m*n)\)

最好情況:

1570613990056

最壞情況:

1570614018748

gs表

相比於bc表,gs表就很不好構造了。首先來看看一個概念,最大匹配后綴長度表,通過它來構建ss(suffix size)表,然后通過ss表來構造gs表。

最大匹配后綴長度的意思是在P[0,j)的所有綴中,與P的某一后綴匹配最長者。例如下面的P[0, 3) = ICE, 與末尾的ICE最長匹配,則P[0, 3)的末尾就為最長匹配長度3,RICE同理。(ss表的值就等於最大匹配長度)

1570614568268

ss表末尾的值就是整個模式串的長度,簡單的想法是遍歷每一個字符向后遞減,與后綴開始一一比較(暴力搜索),這樣做的復雜度為\(O(m^2)\), 很好的做法是下面的代碼(從后往前遍歷),時間復雜度只有\(O(m)\)

vector<int> buildSS ( string P ) { //構造最大匹配后綴長度表:O(m)
    int m = P.size(); 
    vector<int> ss(m, 0); //Suffix Size表
    ss[m - 1]  =  m; //對最后一個字符而言,與之匹配的最長后綴就是整個P串
// 以下,從倒數第二個字符起自右向左掃描P,依次計算出ss[]其余各項
    for ( int lo = m - 1, hi = m - 1, j = lo - 1; j >= 0; j -- )
        if ( ( lo < j ) && ( ss[m - hi + j - 1] <= j - lo ) ) //情況一:該情況處於最大匹配后綴后的字符,例如,RICE中的R,I,C.
            ss[j] =  ss[m - hi + j - 1]; //直接利用此前已計算出的ss[]
        else { //情況二: 遇到匹配項,依次遞減進行匹配
            hi = j; lo = min ( lo, hi );
            while ( ( 0 <= lo ) && ( P[lo] == P[m - hi + lo - 1] ) ) 
                lo--; //逐個對比處於(lo, hi]前端的字符
            ss[j] = hi - lo; // 高位減去遞減后的低位,得到最長匹配長度
        }
    return ss;
}

知道ss表后,gs表可有ss表推導出,有兩種情況:

1570620227448

對應的代碼如下:

vector<int> buildGS ( string P ) { //構造好后綴位移量表:O(m)
   vector<int> ss = buildSS ( P ); //Suffix Size表
   size_t m = P.size(); 
   vector<int> gs(m, m); //Good Suffix shift table
   for ( size_t i = 0, j = m - 1; j < UINT_MAX; j -- ) //逆向逐一掃描各字符P[j]
      if ( j + 1 == ss[j] ) //若P[0, j] = P[m - j - 1, m),則
         while ( i < m - j - 1 ) //對於P[m - j - 1]左側的每個字符P[i]而言
            gs[i++] = m - j - 1; //m - j - 1都是gs[i]的一種選擇
   for ( size_t j = 0; j < m - 1; j ++ ) //正向掃描P[]各字符,gs[j]不斷遞減,直至最小
      gs[m - ss[j] - 1] = m - j - 1; //m - j - 1必是其gs[m - ss[j] - 1]值的一種選擇
   return gs;
}

BM_BC+GS

知道了bc表和gs表,接下來就是匹配過程了,與阮老師的博客上說的一致,取兩個表的最大值。代碼如下:

int match ( string P, string T ) { //Boyer-Morre算法(完全版,兼顧Bad Character與Good Suffix)
   vector<int> bc = buildBC ( P ), gs = buildGS ( P ); //構造BC表和GS表
   size_t i = 0; //模式串相對於文本串的起始位置(初始時與文本串左對齊)
   while ( T.size() >= i + P.size() ) { //不斷右移(距離可能不止一個字符)模式串
      int j = P.size() - 1; //從模式串最末尾的字符開始
      while ( P[j] == T[i + j] ) //自右向左比對
         if ( 0 > --j ) break; 
      if ( 0 > j ) //若極大匹配后綴 == 整個模式串(說明已經完全匹配)
         break; //返回匹配位置
      else //否則,適當地移動模式串
         i += max ( gs[j], j - bc[ T[i + j] ] ); //位移量根據BC表和GS表選擇大者
   }
   return i;
}

基於BM_BC+GS算法最好情況是\(O(n/m)\),最壞情況由於有了gs表,變為了\(O(m+n)\).

綜合性能

各種模式匹配算法的時間復雜度如下所示:

1570622593407

參考

  1. 數據結構鄧俊輝
  2. 阮一峰的KMP算法博客
  3. 阮一峰的BM算法博客


免責聲明!

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



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