串的模式匹配算法
BF(Brute-Force)算法
模式匹配不一定是從主串的第一個位置開始,可以指定主串中查找的起始位置pos。如果采用字符串順序存儲結構,可以寫出不依賴於其他串操作的匹配算法。
算法步驟
-
分別利用計數指針\(i\)和\(j\)指示主串\(S\)和模式串\(T\)中當前正待比較的字符位置,\(i\)初值為 pos,\(j\)初值為 1。
-
如果兩個串均未到串尾,即\(i\)小於等於\(S\)的長度且\(j\)小於等於\(T\)的長度時,則循環執行以下操作:
-
\(S[i]\)和\(T[j]\)比較,若相等,則\(i\)和\(j\)分別指示串中下個位置,繼續比較后序字符;
-
若不等,指針后退重新開始匹配,從主串的下一個字符\((i=i-j+2)\)起重新和模式串的第一個字符\((j=1)\)比較。
-
-
如果\(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指針,而是利用已經得到的“部分匹配”的結果將模式串向右“滑動”盡可能遠的一段距離后,繼續進行比較。
核心思想
移動模式串,使模式串中的公共前后綴里面的前綴移動到后綴的位置。
舉例說明
回顧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函數
- 從頭開始的\(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函數背后的原理
- 求\(next[j+1]\),則已知\(next[1]、next[2]\cdots next[j]\);
- 假設\(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\)位字符重合);
- 如果\(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\),否則進入下一步;
- 此時,\(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\)位字符重合);
- 第二第三步聯合得到:\(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}\)
- 這時候,再判斷如果\(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]\)相同。
//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];
}
}
}