1. 程式人生 > 實用技巧 >資料結構與演算法(十一)——串

資料結構與演算法(十一)——串

iwehdio的部落格園:https://www.cnblogs.com/iwehdio/

1、串

  • 串(字串):來自字母表的字元所構成的有限序列。

  • 一般來說,串長n遠大於字母表中的字元數量。

  • 串相等:串長度相等,且對應位置上的字元相同。

  • 子串:substr(i,k),從s[i]開始的連續k個字元。

  • 字首:prefix(k)=substr(0,k),即最靠前的k個字元。

  • 字尾:suffix(k)=substr(n-k,k),即最靠後的k個字元。

  • 空串長度為0,是任何串的子串、字首、字尾。

  • 串的功能介面:

2、串匹配

  • 一般將所要搜尋的串稱為模式P,搜尋的物件稱為文字T。

  • 模式匹配:

    • 從文字中匹配模式串。
    • 是否出現?
    • 首次在哪裡出現?(主要問題)
    • 共有幾次出現?
    • 各出現在哪裡?
  • 如何評測串匹配演算法的效能?

    • 成功:在T中,隨機取出長度為m的子串作為P;分析平均複雜度。
    • 失敗:採用隨機的P;統計平均複雜度。
  • 蠻力匹配:

    • 自左向右,以字元為單位,依次移動模式串。直到在某個位置,發現匹配。

    • 版本1:

      int match ( char* P, char* T ) { //串匹配演算法(Brute-force-1)
         size_t n = strlen ( T ), i = 0; //文字串長度、當前接受比對字元的位置
         size_t m = strlen ( P ), j = 0; //模式串長度、當前接受比對字元的位置
         while ( j < m && i < n ) //自左向右逐個比對字元
            {
            if ( T[i] == P[j] ) //若匹配
               { i ++;  j ++; } //則轉到下一對字元
            else //否則
               { i -= j - 1; j = 0; } //文字串回退、模式串復位
            }
         return i - j; 
      }
      
  • 版本2:

      int match ( char* P, char* T ) { //串匹配演算法(Brute-force-2)
         size_t n = strlen ( T ), i = 0; //文字串長度、與模式串首字元的對齊位置
         size_t m = strlen ( P ), j; //模式串長度、當前接受比對字元的位置
         for ( i = 0; i < n - m + 1; i++ ) { //文字串從第i個字元起,與
            for ( j = 0; j < m; j++ ) //模式串中對應的字元逐個比對
               {
               if ( T[i + j] != P[j] ) break; //若失配,模式串整體右移一個字元,再做一輪比對
               }
            if ( j >= m ) break; //找到匹配子串
         }
         return i; 
      }
    
  • 唯一的區別就是版本1中i指向文字T中進行比較的位置,版本2中i指向模式T開始比較的位置。

  • 最壞情況下可達到O(m*n)。

  • 低效的原因在於,之前比對過的字元,將在模式T失陪進位後再次比較。

  • KMP演算法:

    • 所要處理的,就是這種一部分字首匹配,但是在某個位置不匹配。對於下圖有T[i]!=P[j],而其字首是匹配的。

    • 因為已經匹配的部分T[i-j,i)P[0,j)是完全相同的。所以,當X!=Y導致失配時,需要將P移動一段距離L,使得T[i-j+L,i)P[0,j-L)匹配。這種情況只與模式P有關,而且對於P中每個字元都只有一種移動的情況。

    • 構造查詢表next[0,m),在任一位置P[j]失敗後,將j替換為next[j]。即相當於P移動了j-next[j]

    • 實現:

      int match ( char* P, char* T ) {  //KMP演算法
         int* next = buildNext ( P ); //構造next表
         int n = ( int ) strlen ( T ), i = 0; //文字串指標
         int m = ( int ) strlen ( P ), j = 0; //模式串指標
         while ( j < m  && i < n ) //自左向右逐個比對字元
            {
            if ( 0 > j || T[i] == P[j] ) //若匹配,或P已移出最左側(兩個判斷的次序不可交換)
               { i ++;  j ++; } //則轉到下一字元
            else //否則
               j = next[j]; //模式串右移(注意:文字串不用回退)
            }
         delete [] next; //釋放next表
         return i - j;
      }
      
    • next表的作用是,藉助必要條件(自匹配),排除不必要的對齊位置比較,實現快速右移。

    • 自匹配:當模式P與文字T,在P[j]處失配時,令t=next[j],進行快速右移。這樣做的條件是,在P[j]的字首P[0,j)中,t是其真字首和真字尾匹配的最大長度,即存在最大的t使得P[0,t)=P[j-t,j)

    • next[0]=-1,是為了在首字元比對失敗的情況下後移一位,對應於後移條件中的 0>j。-1的位置相當於是一個通配哨兵,可以與任何字元匹配。

    • 構造next表:

      • 遞迴的,對於next表中的第j+1項,如果P[j]P[next[j]]匹配(因為next[j]已計算出來,所以字首部分必然匹配),則next[j+1]=1+next[j]。
      • 如果P[j]P[next[j]]不匹配,則需要嘗試P[j]P[next[next[j]]]是否匹配,並可能繼續遞迴。

    • 實現:

      int* buildNext ( char* P ) { //構造模式串P的next表
         size_t m = strlen ( P ), j = 0; //“主”串指標
         int* N = new int[m]; //next表
         int t = N[0] = -1; //模式串指標
         while ( j < m - 1 )
            if ( 0 > t || P[j] == P[t] ) { //匹配
               j ++; t ++;
               N[j] = t; 
            } else //失配
               t = N[t];
         return N;
      }
      
    • 時間複雜度O(n),n為T的規模。

    • 再改進:

      • 對於已經失敗的比對,用同樣的字元再次進行相同的必定失敗的比對,損失了效率。

      • 同樣的字元比對必然失敗,因此可以直接指向next[t]。

      • 實現:

        int* buildNext ( char* P ) { //構造模式串P的next表
           size_t m = strlen ( P ), j = 0; //“主”串指標
           int* N = new int[m]; //next表
           int t = N[0] = -1; //模式串指標
           while ( j < m - 1 )
              if ( 0 > t || P[j] == P[t] ) { //匹配
                 j ++; t ++;
                 N[j] = P[j]!=P[t]? t:N[t]; 
              } else //失配
                 t = N[t];
           return N;
        }
        
  • BM演算法:

    • 不對稱性:判斷串相等和不等的代價是不同的。即使是對單個字元而言,匹配失敗的概率也遠高於成功的概率。
    • 在串匹配中,先對靠後的字元匹配可以獲得更多的教訓。這是因為如果靠後的字元匹配失敗,可以排除較多的對齊位置。
    • 因此,在匹配時,應從模式P從後往前匹配。
  • 壞字元(bc):

    • 模式P從後往前比對時,第一個失配的字元。模式P中為Y,而文字T中為X。X就是壞字元。
    • 這意味著模式P需要右移直到其中的X與文字T中的X對齊。然後再從最右端開始比較。位移量取決於失配位置和X在P中的秩,可製表bc待查。
    • 如果模式P中的X在匹配位置的右側,則模式P只向右移動一個位置。
    • 如果模式P中不包含X,則直接完全越過這個對齊位置。
    • 如果模式P中存在多個X,則先選擇字首中秩最大的。

    • 構造bc表:記錄所有字元最後一次出現的位置。

    • 實現:

      int* buildBC ( char* P ) { //構造Bad Charactor Shift表:O(m + 256)
         int* bc = new int[256]; //BC表,與字元表等長
         for ( size_t j = 0; j < 256; j ++ ) bc[j] = -1; //初始化:首先假設所有字元均未在P中出現
         for ( size_t m = strlen ( P ), j = 0; j < m; j ++ ) //自左向右掃描模式串P
            bc[ P[j] ] = j; //將字元P[j]的BC項更新為j(單調遞增)——畫家演算法
         return bc;
      }
      
    • 時間複雜度最好O(n/m),最壞O(n*m)。

  • 好字尾(gs):

    • 在模式P的字尾G完成與文字T中的子串S的匹配時,可以將完成匹配的部分看作經驗。G就是好字尾。
    • 如果模式P還有一部分需要與子串S匹配,則P中與之匹配的部分必然與G完全相同。
    • 可以通過製表gs,當好字尾繼續匹配失敗時,移動模式P使得相同的字元與子串S對其。
  • 位移量根據bc表和gs表選擇較大者。

  • 構造gs表:

    • MS[]子串:P[0,j]的所有後綴中,MS[j]是與P的某一字尾匹配的最長者。

    • ss[]表:ss[j]是MS[j]的長度。其中包含了gs表的所有資訊。

    • 從ss[]表到gs[]表:

      • 如果s[j]=j+1,則對任一字元P[i](i<m-j-1),m-j-1必是gs[i]的一個候選。
      • 如果s[j]<=j,則對字元P[m-ss[j]-1],m-j-1必是gs[m-ss[j]-1]的一個候選。
      • 注意這裡第一種情況是對在好字尾之前的任意的失配字元,而第二種情況只是對好字尾之前的哪一個失配字元。

  • 時間複雜度最好O(n/m),最壞O(n+m)。

  • KR演算法:

    • 將字串轉化為整數,這樣一次比較只需要O(1)的實現。
    • 對於一個字母表規模為d的字串,都可以對應於一個d進位制的自然數。
    • 但是這樣所需的儲存空間太大,可以通過雜湊的方法壓縮。
    • 先比較雜湊碼,雜湊衝突時,再進行串中字元的逐一匹配。
    • 子串的雜湊過程,可以根據相鄰子串間的相似性,通過移位快速得到。

iwehdio的部落格園:https://www.cnblogs.com/iwehdio/