演算法:模式匹配之KMP演算法
前言:
昨天看到《演算法導論》裡的第32章:字串匹配,說到一個關於字串匹配的很好的演算法——KMP。關於KMP的記憶體含意以及KMP的來源,不是本文講述的範疇,請感興趣的讀者自行查閱相關資料。
本文主要是來說明KMP演算法的思路和實現過程,以及它相比於樸素的字串模式匹配存在的優勢。
--轉載請註明出處
樸素模式匹配演算法:
1.思路分析
樸素的字串模式匹配就是傳統的演算法——逐個比較。因為使用了巢狀迴圈,所以效率比較低。
2.程式碼實現
public class SimpleMatching { /** * 採用樸素的字串匹配演算法查詢子字串 * * @param T * 主字串 * @param P * 匹配模式字串 * @return * 匹配成功的所有位置 */ public List<Integer> getIndexOfPinT(String T, String P) { if (Tools.isEmptyString(T) || Tools.isEmptyString(P)) { return null; } List<Integer> indexs = new ArrayList<Integer>(); char[] t = T.toCharArray(); char[] p = P.toCharArray(); for (int i = 0; i <= t.length - p.length; i++) { for (int j = 0; j < p.length; j++) { if (t[i + j] == p[j]) { if (j == p.length - 1) { indexs.add(i); } continue; } break; } } return indexs; } }
我們假定主字串的長度為n,匹配模式字串的長度為m。那麼對於上面的演算法,時間複雜度就是O(m*n)。對於一些需求不太嚴苛或是m,n比較小的情況下。這種演算法還是可以接受的。但是如果不是上述情況,這樣的一個時間複雜度可能還是略顯尷尬。下面我就來介紹一下改進過後的KMP演算法。
KMP模式匹配演算法:
1.思路分析
KMP演算法的關鍵是為我們排除了一些重複匹配,使用主字串的匹配位置“指標”不需要回溯。這裡不妨列舉一個小例子。
主字串T:fababadaaswababaca
匹配模式P:ababaca
假使此時我們正在匹配T的第7位(fababa[d]aaswababaca)和P的第6位(ababa[c
我們重新檢查一下P(ababaca),當我們開始匹配第6位的時候,之前的5位已經匹配完成。而且,[aba]baca = ab[aba]ca!那麼針對於T而言,fab[aba]daaswababaca這三位是已經匹配過了,我們在把與之前匹配相等的字串移至此處時,是不是就說明,這幾個字串是不需要再匹配了。
當我們知道了在匹配的過程中,有一些字元是不需要再匹配了的時候,接下來就是重頭戲了。如何讓這些已經匹配過的字串不再重複匹配?
通過上面的分析,其實已經暗含瞭解決方案。就是我們要知道匹配模式中,每個最優字首(關於最優字首可以參考《演算法導論》32章內容)S中,S的不為自身的最長的一個等於最優字尾(關於最優字尾可以參考《演算法導論》32章內容)的最優字首SS。這句話可能聽起來有一些繞口,下面通過一個例項來說明:
匹配模式P:ababaca
我們選取P的一個最優字首S = ababa,那麼SS = aba.因為SS是S的最優字首,也是S的最優字尾,而且是最長的。
上面就是關於KMP匹配模式的思路分析,如果你感覺這裡有一些枯燥乏味或是閱讀困難(對此,我對我拙劣的文字描述表示一些歉意)。下面可以在程式碼中尋找大家所契合的地方,因為邏輯是想通的嘛。
2.最優字首的形式:
Index 0123456
P ABABACA
Next 0012301
3.程式碼實現
(1)獲得字串中的每個最優字首子字串中的最長的最優字首等於最優字尾的長度
/**
* 獲得字串中的每個最優字首子字串中的
* 最長的最優字首等於最優字尾的長度
*
* @param text
* 待計算的字串
* @return
* 返回最長的最優字首等於最優字尾的長度陣列
*/
public int[] getNext(String text) {
if (Tools.isEmptyString(text)) {
return null;
}
int[] lengths = new int[text.length()];
for (int i = 0; i < text.length(); i++) {
String sub = text.substring(0, i + 1);
int maxLen = 0;
for (int j = 0; j < sub.length() - 1; j++) {
String subChild = sub.substring(0, j + 1);
if (sub.endsWith(subChild) && subChild.length() > maxLen) {
maxLen = subChild.length();
}
}
lengths[i] = maxLen;
}
return lengths;
}
(2)獲得字串P在字串T中出現的所有位置
/**
* 獲得字串P在字串T中出現的所有位置
*
* @param T
* 主字串
* @param P
* 匹配模式字串
* @return
* 匹配成功的所有位置
*/
public List<Integer> getIndexOfPinT(String T, String P) {
if (Tools.isEmptyString(T) || Tools.isEmptyString(P)) {
return null;
}
List<Integer> indexs = new ArrayList<Integer>();
char[] t = T.toCharArray();
char[] p = P.toCharArray();
int[] next = getNext(P);
int indexT = 0;
int indexP = 0;
while (indexT < t.length) {
if (t[indexT] == p[indexP]) {
indexP++;
indexT++;
} else {
if (indexP == 0) {
indexT++;
} else {
indexP = next[indexP - 1];
}
}
if (indexP == p.length) {
indexs.add(indexT - indexP);
indexP = 0;
}
}
return indexs;
}
參考說明:
1.《演算法導論》
原始碼下載:
對於上面的描述,如果你還沒有完全理解,可以在下面的連結中下載與本文相關的原始碼進行參考學習.