串的模式匹配算法


串的模式匹配算法

BF(Brute-Force)算法

模式匹配不一定是從主串的第一個位置開始,可以指定主串中查找的起始位置pos。如果采用字符串順序存儲結構,可以寫出不依賴於其他串操作的匹配算法。

算法步驟

  1. 分別利用計數指針\(i\)\(j\)指示主串\(S\)和模式串\(T\)中當前正待比較的字符位置,\(i\)初值為 pos,\(j\)初值為 1。

  2. 如果兩個串均未到串尾,即\(i\)小於等於\(S\)的長度且\(j\)小於等於\(T\)的長度時,則循環執行以下操作:

    • \(S[i]\)\(T[j]\)比較,若相等,則\(i\)\(j\)分別指示串中下個位置,繼續比較后序字符;

    • 若不等,指針后退重新開始匹配,從主串的下一個字符\((i=i-j+2)\)起重新和模式串的第一個字符\((j=1)\)比較。

  3. 如果\(j\)大於\(T\)的長度,說明模式串\(T\)中的每個字符依次和主串\(S\)中的一個連續的字符序列相等,則匹配成功,返回和模式串\(T\)中第一個字符相等的字符在主串\(S\)中的序號\((i-T的長度)\);否則稱匹配不成功,返回0。

下面的一系列圖展示了模式串\(T="abcac"\)和主串\(S\)的匹配過程(pos = 1)。


算法實現

int StrIndex_BF(SString S,SString T,int pos)
{
    int i,j;
    if(pos <= 1 && pos <=S[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;
            }
        }
        if(j > T[0])
            return i - T[0];
        else
            return 0;
    }
    else{
        return 0;
    }
}


KMP算法

定義

該算法是由Knuth、Morris和Pratt同時設計實現的,因此簡稱KMP算法。其相較於BF算法,改進在於:每當一趟匹配過程中出現字符比較不相等時,不需要回溯i指針,而是利用已經得到的“部分匹配”的結果將模式串向右“滑動”盡可能遠的一段距離后,繼續進行比較。

核心思想

移動模式串,使模式串中的公共前后綴里面的前綴移動到后綴的位置。

舉例說明

\[\begin{array}{clcr} 主串 &a&b&c&a&a&b&b&c&a&b&c&a&a&b&d&a&b\ \end{array}\]

\[\begin{array}{clcr} 模式串 &a&b&c&a&c\ \end{array}\]

回顧BF匹配算法的過程示例,在第三趟的匹配中,當\(i=7、j=5\)字符比較不等時,又從\(i=4、j=1\)重新開始比較。然后,經自習觀察可以發現,\(i=4\)\(j=1\)\(i=5\)\(j=1\),以及 \(i=6\)\(j=1\)這三次比較都是不必進行的。因為從第三趟部分匹配的結果就可以得出,主串中第4個、第5個和第6個字符必然是“b”、“c”和“a”(也就是模式串中的第2個、第3個和第4個字符)。因為模式串中的第一個字符是“a”,因此它無需再和這3個字符進行比較,而僅需將模式串向右滑動3個字符的位置繼續進行\(i=7、j=2\)時的字符不比較即可。同理,在第一趟匹配中出現字符不等時,僅需將模式串向右移動兩個字符的位置繼續進行\(i=3、j=1\)時的字符比較。由此,在整個匹配的過程中,\(i\)指針沒有回溯,如下圖所示。


現在再來討論一般情況。假設主串為\("s_1s_2s_3\ldots s_n"\),模式串為\("t_1t_2t_3\ldots t_m"\),從上面的分析可以知道,為了實現改進算法,需要解決下述問題:當匹配過程中產生“失配”(即\(s_i\neq t_j\))時,模式串“向右滑動”可行的距離多遠,換句話說,當主串中第\(i\)個字符與模式串中第\(j\)個字符“失配”(即比較不等)時,主串中第$ i \(個字符(\)i$指針不回溯)應與模式串中哪個字符再比較。

假設此時應與模式串中第\(k\)\(k<j\))個字符繼續比較,則模式串中前\(k-1\)個字符的子串必須滿足下列關系式,且不可能存在\(k^ ,>k\)滿足下列關系式:

​ $$ "t_1t_2t_3\ldots t_{k-1}"="s_{i-k+1}s_{i-k+2}\ldots s_{i-1}"\quad\quad\quad\quad\quad(1)$$

而已經得到的"部分匹配"的結果是:

​ $$ "t_{j-k+1}t_{j-k+2}\ldots t_{j-1}"="s_{i-k+1}s_{i-k+2}\ldots s_{i-1}"\quad\quad\quad\quad\quad(2) $$

由上面兩個式子可以推得下列等式:

​ $$ "t_1t_2\ldots t_{k-1}"="t_{j-k+1}t_{j-k+2}\ldots t_{j-1}" \quad\quad\quad\quad\quad (3) $$

反之,若模式 串中存在滿足式(3)的兩個子串,則當匹配過程中,主串中第\(i\)個字符與模式串中第\(j\)個字符比較不等時,僅需將模式串向右滑動至模式串中第\(k\)個字符和主串中第\(i\)個字符對齊,此時,模式串中頭\(k-1\)個字符的子串\(t_1t_2\ldots t_{k-1}\)必定與主串中第\(i\)個字符之前長度為\(k-1\)的子串\(“s_{i-k+1}s_{i-k+2}\ldots s_{i-1}”\)相等,由此,匹配僅需從模式串中第\(k\)個字符與主串中第\(i\)個字符開始,依次向后進行比較。


視頻學習地址:KMP算法通俗易懂視頻

實現

若令\(next[j]=k\),則\(next[j]\)表明當模式串中第\(j\)個字符與主串中相應字符“失配”時,在模式串中需重新和主串中該字符進行比較的字符的位置。

next函數

\[next[j] = \begin{cases} 0 & \text {當 $j = 1$ 時}\\ max\lbrace k|1<k<j,且{\overbrace{"p_1\cdots p_{k-1}"}^{從頭開始的k-1個元素}= \overbrace{"p_{j-k+1}\cdots p_{j-1}"}^{j前面的k-1個元素}}\rbrace& \text {當$此集合非空$時}\\ 1 & \text {$其他情況$}\\ \end{cases}\]

  • 從頭開始的\(k-1\)個元素就是字符串的前綴
  • \(j\)前面的\(k-1\)個元素就是字符串的后綴

舉例如下:


在求得模式串的\(next\)函數之后,匹配可如下進行:假設以指針\(i\)\(j\)分別指示主串和模式串中正待比較的字符,令\(i\)的初值為pos,\(j\)的初值為1。若在匹配過程中\(S_i=T_j\),則\(i\)\(j\)分別增1,否則,\(i\)不變,而\(j\)退到\(next[j]\)的位置再比較,若相等,則指針各自增1,否則\(j\)再退到下一個\(next\)值得位置,依次類推,直至下列兩種可能:一種是\(j\)退到某個\(next\)\((next[next[...next[j]...]])\)時字符比較相等,則指針各自增1,繼續進行匹配;另一種是\(j\)退到值為零(即模式串的第一個字符失配),則此時需將模式串繼續向右滑動一個位置,即從主串的下一個字符\(S_{i+1}\)起和模式串重新開始匹配。下圖所示正是這段話匹配過程的一個例子。


算法實現

//KMP算法實現過程
int StrIndex_KMP(SString S,SString T,int pos)
{
    int i,j;
    if(1 <= pos && pos <= S[0]){
        i = pos;
        j = 1;
        while(i <= S[0] && j <= T[0]){
            if( j == 0 || S[i] == T[j]){
                i++;
                j++;
            }
            else{
                j = next[j];
            }
        }
        if(j > T[0])
            return i - T[0];
        else
            return 0;
    }
    esle{
        return 0;
    }
}
//next函數求取過程
void Get_Next(SString T,int next[])
{
    int i = 1,j = 0;
    next[1] = 0;
    while(i < T[0]){
        if(j == 0 || T[i]==T[j]){//T[i]表示后綴的單個字符,T[j]表示前綴的單個字符
            i++;
            j++;
            next[i] = j;
        }
        else{
            j = next[j];//若字符不相同,則j值回溯至字符相等或者j=0處
        }
    }
}
next函數背后的原理
  1. \(next[j+1]\),則已知\(next[1]、next[2]\cdots next[j]\);
  2. 假設\(next[j]=k\),則有\(t_1t_2\cdots t_{k-1}=t_{j-k+1}t_{j-k+2}\cdots t_{j-1}\)(前\(k-1\)位字符與后\(k-1\)位字符重合);
  3. 如果\(t_k=t_j\),則有\(t_1t_2\cdots t_{k-1}t_k=t_{j-k+1}t_{j-k+2}\cdots t_{j}\),則此時\(next[j+1]=k+1\),否則進入下一步;
  4. 此時,\(t_k\neq t_j\),假設\(next[k] = k_1\),則有\(t_1\cdots t_{k_1-1}=t_{k-k_1+1}\cdots t_{k-1}\)(前\(k_1-1\)位字符與后\(k_1-1\)位字符重合);
  5. 第二第三步聯合得到:\(t_1\cdots t_{k_1-1}=t_{k-k_1+1}\cdots t_{k-1}=t_{j-k_1+1}\cdots t_{k_1-k+j-1}=t_{j-k_1+1}\cdots t_{j-1}\)
  6. 這時候,再判斷如果\(t_{k_1}=t_j\),則\(t_1\cdots t_{k_1-1t_{k1}=t_{j-k_1+1}\cdots t_{j-1}t_j}\),則\(next[j+1]=k_1+1\);否則再取\(next[k_1]=k_2\)
圖解原理

2.求\(next[17]\),已知了\(next[16]\),假設\(next[16]=8\),此時則有\(t_1t_2\cdots t_8=t_9t_{10}\cdots t_{15}\),分兩種情況討論:

  • 如果\(t_8=t_{16}\),則非常明顯可以看出\(next[17]=8+1=9\)

  • 如果\(t_8\neq t_{16}\),再假設\(next[8]=4\),則有下圖所示的關系:

從而又可以推出下圖中所示的四個部分重合,此時需要再進行判斷\(t_{16}\text 和t_4\)的關系,同樣需要分為兩種情況

  • 如果\(t_4=t_{16}\),從上圖可以看出有:\(t_1\cdots t_4=t_{13}\cdots t_{16}\),則此時\(next[17]=4+1=5\)

  • 如果\(t_4\neq t_{16}\),再假設\(next[4]=2\),此時有下圖所示的關系:

  • 此時,如果\(t_2=t_{16}\),則\(next[17]=2+1=3\)
  • 如果\(t_2\neq t_{16}\)\(t_2=1\)\(next[1]=0\),遇到0時還沒有結束,則遞推結束,此時\(next[17]=1\)

視頻學習地址:next函數代碼理解視頻

算法的改進

前面定義的\(next\)函數在某些情況下尚有缺陷。例如模式串為“aaaab”在和主串“aaaabaaaab”匹配時,當\(i=5、j=5\)時,\(S[5]\neq T[5]\),由\(next[j]\)的指示還需進行\(i=5、j=4,i=5、j=3,i=5、j=2、i=5、j=1\)這四次比較。而實際上,因為模式串中第1~3個字符和第4個字符都相等,因此不需要再和主串中第4個字符相比較,而可以將模式串向右滑動4個字符的位置直接進行\(i=5、j=1\)時的字符比較。這就是說,若按上述定義得到\(next[j]=k\),而模式串中\(t_j=t_k\),則當主串中字符\(S_i\)\(T_j\)比較不等時,不需要再和\(T_k\)進行比較,而直接和\(T_{next[k]}\)進行比較,換句話說,此時的\(next[j]\)應和\(next[k]\)相同。

\[\begin{array}{c|lcr} j & \text{1} & \text{2} & \text{3} & \text{4}& \text{5}& \text{6}\\ \hline 模式串 & a & a & a & a & a & b\\ next[j] & 0 & 1 & 2 & 3 & 4 & 5 \\ nextval[j] & 0 & 0 & 0 & 0 & 0 & 5 \\ \end{array} \]

\[\begin{array}{c|lcr} j & \text{1} & \text{2} & \text{3} & \text{4}& \text{5}& \text{6}&\text{7}&\text{8}&\text{9}\\ \hline 模式串 & a & b & a & b & a & a & a & b & a \\ next[j] & 0 & 1 & 1 & 2 & 3 & 4 & 2 & 2 & 3\\ nextval[j] & 0 & 1 & 0 & 1 & 0 & 4 & 2 & 1 & 0\\ \end{array} \]

//nextval函數求取過程
void Get_Next(SString T,int nextval[])
{
    int i = 1,j = 0;
    nextval[1] = 0;
    while(i < T[0]){
        if(j == 0 || T[i]==T[j]){
            i++;
            j++;
            if(T[i] != T[j])
                nextval[i] = j;
            else
                nextval[i] = nextval[j];
        }
        else{
            j = nextval[j];
        }
    }
}


免責聲明!

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



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