在對字符串的操作中,我們經常要用到子串的查找功能,我們稱子串為模式串,模式串在主串中的查找過程我們成為模式匹配,KMP算法就是一個高效的模式匹配算法。KMP算法是蠻力算法的一種改進,下面我們先來介紹蠻力算法。
蠻力算法使用兩個int型變量當做當前匹配位置的指針,我們假設主串的位置指針為i,模式串的位置指針為j。蠻力算法的策略便是在i和j所指的位置的字符相等時,繼續向后匹配,當發生失配時,便將i回溯到本次匹配前位置的后一個位置,而將j設置為0,從而對所有位置完成逐一比對,通過觀察i和j是否越界判斷整體匹配是否成功,若模式串位置指針j越界,顯然此前所有位置都已完全匹配,那么也就可以返回i-j也即完全匹配時主串中匹配子串的下標位置。
int brute(char * mString ,char * subString) { int i = 0 ,j = 0; int m = strlen(mString), n = strlen(subString); while ( i < m &&j < n) { if (mString[i] == subString[j]) { i++; j++; } else { i -= j - 1; j = 0; } }if (j >= n) return i - j;
if(i>=m) return -1;
}
通過觀察,我們不難發現,如果主串中有大量與模式串相似的字符,從而每次比對都要比較到模式串的最后一個字符才發生失配,從而在每個比較位置都與模式串進行模式串長度n次的比較,而一共有主串長度m次的比較位置。從而時間復雜度達到了O(m*n)。這種情況在串中字符的種類較少時尤其容易發生,如主串為“00000000000001”,模式串為“00001”。
顯然,在每次發生失配時,i指針都要回溯到原來位置的下一個位置,而j指針則是復位至0,一切又從下一個位置從新開始。然而這種做法其實浪費了大量之前比較時所獲得的有用信息。在發生失配時,可以分為兩種情況:主串失配字符位置i之前的若干字符如果和模式串中的某個真前綴相同(為了避免丟失信息,必須選擇最長的一種情況),那么顯然只需將模式串與主串中對應字符對齊,而在當前位置與模式串的這個真前綴后的那個字符進行比較即可;而如果不能找到這樣的情況,顯然主串失配位置前的字符都無法派上用場,從而直接將模式串的開頭與當前位置對齊並進行比較即可(注意,如果這種情況中開頭位置依然失配,只需讓i自增,從下一個位置開始和整個模式串的匹配即可)。這兩種情況中的i指針始終沒有回溯。因此在最壞情況下的時間復雜度也不會超過O(m)。
int KMP(char *mString ,char* subString) { int i = 0,j = 0; int mlen = strlen(mString); int slen = strlen(subString); int* next = (int*)malloc(sizeof(int)*strlen(subString)); getNext(subString, next); while (i < mlen && j < slen) { if (mString[i] == subString[j]) i++, j++; else { if (j == 0)i++; j = next[j];//子串指針前移至最長公共前后綴的下標處 } } if(j>=slen) return i - j; if (i >= mlen) return -1; }
細心的讀者可能發現,這里與蠻力算法不同的是多了一次主串適配位置之前字符與模式串前綴字符的比較操作,這樣看來似乎復雜度沒有改善,其實不然,由於發生失配時主串當前位置i和模式串當前位置j之前的所有字符必然相等,所以這種比較操作只取決於模式串,也就是說我們只需找出模式串每個位置的最長公共前后綴即可。我們只需在比較之前對模式串進行分析處理,將對應信息存入next[]數組來制表以供查詢即可將這種操作簡化為O(1)的復雜度。而這種預處理操作實際上只需O(n)的復雜度。
next[]數組的獲取我們可以使用遞推的策略完成,由於next[]數組中存放的數值為當前位置最長公共前后綴的長度,也即最長公共前綴之后一個字符的下標位置,顯然如果之前位置字符與之前位置的最長公共前綴的下一個字符相同,那么當前位置的最長公共前后綴的長度只需增加一即可(特別的,如果當前位置和當前位置最長公共前后綴的后一個字符相等,說明這次比較必然失敗,於是應該取其此處不相等的最長公共前后綴);而如果這兩個字符不同,我們希望找到之前字符稍短的一個公共前后綴,也即在之前字符的next[]值上再取一次next[]的值,再比較這個值處的字符和之前位置處字符,如果相等取此值加一即可,如果不相等,則繼續循環,由於next[]數組中的值必然比數組內值小,所以循環一直繼續下去必然收斂於0。當值為0時。取當前值為0即可。
void getNext(char * string, int * next) { int len = strlen(string); int i = 0,j = 0; next[0] = 0; while (j < len) { if (i == 0) next[++j] = 0; if (string[j] == string[i]) { i++; j++; next[j] = string[j]!=string[i]?i:next[i];//避免出現重復比較 } else i = next[i]; } }
純屬個人理解,如有錯誤,歡迎指出
