串的模式匹配演算法(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_;
}
}