1. 程式人生 > >串的模式匹配演算法(BF演算法和KMP演算法)

串的模式匹配演算法(BF演算法和KMP演算法)

串的模式匹配演算法

子串的定位操作通常稱為串的 模式匹配,其中T稱為 模式串

一般的求子串位置的定位函式(Brute Force)

我寫java的程式碼是這樣的

int index(String S,String T,int pos){
    char[] s_arr = S.toCharArray();
        char[] t_arr = T.toCharArray();
        int i,j,k;//i是主串S的指標,j是模式串的指標
        if(pos < 0 || pos>S.length() || 
                S.length
() < T.length() || pos+T.length()>S.length()) return -2; /*最外層是與主串匹配的最多次數,從陣列下標為0開始匹配,i表示當前匹配是 從主串下標為i的元素開始的*/ for(i = pos-1;i < S.length()-T.length();i++){ /*內層迴圈是模式串的迴圈,j表示當前匹配指標的位置*/ for(j=0;j<T.length();j++){ /*i+j是主串上指標的位置,如果兩者不匹配,則將主串的指標挪後 一個位置,模式串指標從頭開始,重新匹配*/
if(s_arr[i+j] != t_arr[j]){ break; } } /*與模式串的匹配結束,判斷模式串的指標位置*/ if(j>=T.length()) return i; } return -1; }

書上的演算法是這樣的(偽碼)

int index(String S,String T,int pos){
    /*返回子串T在主串S中第pos位置字元後的位置,若不存在,返回值為0
其中T非空,1<=pos<=S.length,S[0]和T[0]位置儲存字串長度 */ i = pos;j = 1; while(i <= S[0] && j <= T[0]){ if(S[i] == T[j]){ i++;j++/*兩個串的指標後退*/ } else{ i = i-j+2;j = 1;/*主串的指標後退i-j+2個單位,由於下標從1開始 主串中j-i+1的位置與模式串1位置對應,再向後挪一個單位得。*/ } } if(j>T[0]) return i-T[0]; return 0 }

當主串長度為m,模式串長度為n時(m>n),BF的時間複雜度O(m*n)。

改進後的模式匹配演算法

由於在BF演算法中,每一次和主串匹配過程中遇到不配字元後,模式串指標總是要回溯到一開始重新和主串的下一個字元開始匹配,期間可以利用以及得到的“部分匹配”結果,將模式串“向右滑動”一段儘可能遠的距離,繼續比較。其實還是將當前主串的指標所指的字元,與模式串中“部分匹配”的內容中某一個字元繼續匹配。
現在的問題就變成了:
* 當前主串所指的字元,應該模式串中完成“部分匹配”的子字串中哪一個字元繼續比較,即向右滑動的距離。

主串S下標 1,2,…,i-j+1,…,i-1,i,…,m

模式串T下標 1,…,j-1,j,…,n

當前指標i和j所指的字元在進行匹配(未判斷二者是否相等)。

這樣的情況下有這樣的等式:s(i-j+1)…s(i-1) = t(1)…t(j-1),是對應匹配的。

那麼假設s(i)和t(j)不匹配,模式串需要右移,右移動後,此時s(i)和t(k)正在匹配(這裡的k是假設出來的)

有這樣的等式:s(i-k+1)…s(i-1) = t(1)…t(k-1)

形象的描述

S : a a b a b c d e   i指向c
T1:   a b a b e       j指向e
T2:       a b a b e   右移後,j指向a(下標為3),即第k個字元的位置

可以看出來,模式串中,從開始第一個字元起的一段子串——長度為k-1的子串(ab),與以當前匹配字元的前一個字元為結尾的一段子串(ab),長度也是k-1,是有相等關係的,對於模式串,我們得出這樣一個公式:

t(1)…t(k-1) = t(j-k+1)…t(j-1)

那麼正是這長度為k-1的模式串的子串,決定了在遇到不匹配字元時,模式串指標重定位到第k個字元。
現在的問題就變成了:
* 尋找模式串中下標為j的字元前的子串中長度為k-1的子串
* 使得存在t(1)…t(k-1) = t(j-k+1)…t(j-1)這樣的關係

注意這兩段可匹配的子串,必須要有這樣幾個條件:
* 在下標為j的字元前。
* 第一段子串的開頭元素必須是模式串的開頭元素,第二段子串的結束元素必須是j-1為下標的元素。

看上去就是“掐頭去尾”中的頭和尾。

我們用到了next陣列,next[j]=k表示在模式串中,第j個元素與主串中的元素失配後,模式串指標應當指向下標為k的模式串字元,重新和主串中相應字元比較。

栗子:

  j       1  2  3  4  5  6  7  8

模式串    a  b  a  a  b  c  a  c

next[j]   0  1  1  2  2  3  1  2  

到底怎麼算next[j]呢?我昨晚上問了實驗室的一個同學,他告訴我一方法:

規定,next[1] = 0。

在計算next[j]的時候,用手指擋住下標為j的字元,看之前的字元中有沒有頭尾相等的子串

如果有,那麼這兩個相等的子串就是我們找的長度為k-1的串。那麼k=子串長度+1,這樣就計算出來next[j]的值啦。

那麼如果沒有,說明k-1=0咯,那麼k就是1。

此時,優化演算法就成了:

int index(String S,String T,int pos){
    /*返回子串T在主串S中第pos位置字元後的位置,若不存在,返回值為0
    其中T非空,1<=pos<=S.length,S[0]和T[0]位置儲存字串長度
    */
    i = pos;j = 1;
    while(i <= S[0] && j <= T[0]){
        if(S[i] == T[j]){
            i++;j++/*兩個串的指標後退*/
        }
        else{
            j=next[j];
        }
    }
    if(j>T[0])
        return i-T[0];
    return 0

}

next陣列到底怎麼求

通過上面的討論,可以知道,next陣列和主串無關,只和模板串自身有關。按照教材上的說明,是由定義出發,通過遞推的方式求得next函式值。

這裡可以看做是模板串自己和自己進行比較。因為每尋找一個下標的next數值,都是在該下標之前的模式串的子串中尋找、比較看看裡面有沒有符合條件的兩個長度為k-1的、相配的子串。

由定義知道 next[1] = 0

在和主串進行比較進行回退尋找k的時候,有這樣的關係:

t(1)…t(k-1) = t(j-k+1)…t(j-1)

上面這個式子成立的時候,模式串的第j個字元和主串的第i個字元失配,模式串回退到第k個字元的位置,其中k的取值應該是在1和j之間並且這兩段k-1長度的字串應該是在j之前的子串中最大的一組,即不存在k1使得k < k1 (當然k1也小於j)。

那麼,在求next[j+1] = ? 的時候,就會有兩種情況。

情況一,t(k) = t(j)

t(1)…t(k-1)t(k) = t(j-k+1)…t(j-1)t(j)

那自然就是這兩個長度為k-1的串又增加1了唄,即如果在這個位置(j+1)與主串發生了失配,那麼應該把指標指向k+1位置的字元。即

next(j+1) = next(j) + 1

情況二,t(k) != t(j)

這個時候該怎麼辦呢?注意,此時仍然有

t(1)…t(k-1) = t(j-k+1)…t(j-1)

此時將t(1)…t(k)拿出來(複製出來),和原本的模式串比較(分別稱呼主串和模式串)

t(1) ... t(k-1) ... t(k) ... t(j-k+1) ... t(j-1) t(j) ...
                             t(1)     ... t(k-1) t(k)

此時的話,下面的式子需要向右滑動了,這個的分析和上面分析k的原理類似,我們假設此時應該將指標滑動到下標為next[k]=k2的字元上,用之和t(j)相比較。
注意,t(1)…t(k-1) = t(j-k+1)…t(j-1)

t(1) ... t(k-1) ... t(k) ... t(j-k+1) ...t(j-k2+1) ... t(j-1)  t(j) ...
                                         t(1) .......  t(k2-1) t(k2) ... t(k)
若此時的t(k2) = t(j)

則說明在主串中的j+1的位置前存在一個長度為next[k]的最長子串,和模式串中從首字元起長度為next[k]的子串相等(模式串就是主串的一部分)。於是就有了

next[j+1] = next[k]+1

又從求k的過程我們知道next[j] = k。所以上式又可以寫作:

next[j+1] = next[ next[j] ] + 1

若此時t(k2) != t(j)

此時又需要右移了。。。需要再求next[k2]了

t(1) ... t(k-1) ... t(k) ... t(j-k+1) ...t(j-k2+1) ... t(j-1)  t(j) ...
                                         t(1) .......  t(k2-1) t(k2) ... t(k)

所以,按照上面兩種情況的推理過程,得出了一個結論,next函式值是需要依靠之前的位置元素的next函式值來確定的。

寫出來next函式演算法

void getNext(String T,int next[]){
    i =1;next[1] = 0;j = 0;
    while(i<T[0]){
        /*若j是0說明這是剛剛開始,就將next[1]=0
        移動兩個指標,再比較
        若是j不是0,但是兩個指標指向的字元相同,移動指標再比較*/
        if( j == 0 || T[i] == T[j]){
            j++;
            /*這裡要注意的是,從一開始i和j的值就是不一樣的,第一次執行的時候
            next[2] = 1*/
            /*將i+1位置的next值置為j*/
            next[i+1] = j;
            i++;
        }
        else
            j = next[j];
    }
}

以上大概花了我兩天的時間。。第一天理解,第二天寫筆記,寫的時候還是發現有不理解的地方,比如將上面的原理轉換為演算法程式碼,我就還是沒理解。。。

—————-2015年3月16日19:10:35補充———————
新增java實現

//Test.java
public class Test {
    public static void main(String[] args){
        String S = "ababaabcacbab";
        String T = "abaabcac";
        int[] next = getNext(T);
        int i = index_KMP(S,T,0,next);
        System.out.println(i);

    }

    public static int index_KMP(String S,String T,int pos,int[] next){

        int i = pos;
        /*從陣列t的第一個字元開始比較,所以j的初始值是0*/
        int j = 0;
        char[] s_arr = S.toCharArray();
        char[] t_arr = T.toCharArray();

        while(i<S.length() && j<T.length()){
            /* 當j=-1的時候,說明當前模式串的指標在上一個while迴圈中被賦值next[j]
             * 表明模式串的第一個字元和主串中i指向的字元不匹配,那麼需要將兩個指標統統
             * 後移一個單位。
             * 後一個情況就好理解了,兩個字元匹配,指標統統後移。
             * */
            if(j == -1 || s_arr[i] == t_arr[j]){
                i++;
                j++;
            }
            else{
                /*模式串指標前移*/
                j = next[j];
            }
        }
        //如果j的值等於了模式串的長度,說明匹配到了相同子串,返回該子串第一個字元在主串中的下標
        if(j>=T.length())
            return i-T.length();

        return -1;
    }

    public static int[] getNext(String T){
        int[] next = new int[T.length()+1];
        char[] t_arr = T.toCharArray();
        /*next陣列是從下標為1開始的,i作為next的下標,初值為1*/
        int i = 1,j = 0;
        next[0] = next[1] = 0;
        while(i<T.length()){
            /*如果j=0,則說在上一次迴圈中失配,主串i位置對應的next值
             * 應當為1
             * 如果字元匹配,那麼指標雙雙後移,next陣列對應下標的值也+1
             * 由於當前字串下標從0開始,所以需要i-1和j-1
             * */
            if(j == 0 || t_arr[i-1] == t_arr[j-1]){
                j++;
                i++;
                next[i] = j;        
            }
            else
                j = next[j];
        }
        /*由於下標是從1開始的,所以不適合從0開始下標,將next陣列中的
         * 索引和索引位置的元素統統-1,除了下標為0的元素仍然為0*/
        int[] next_ = new int[T.length()];
        for( i=0;i<T.length();i++)
            next_[i] = next[i+1]-1;
        next_[0] = 0;
        return next_;
    }

}