一、KMP是什麼
KMP算法是爲了解決字符串匹配效率而提出的,提出者爲D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位大牛,故稱爲“KMP”算法。
二、暴力求解算法
1、題目:假設一個父字符串是father,子字符串是son,在father中查找son,如果存在則返回son在father中的起始索引,不存在則返回-1。
2、最簡單的解法就是使用循環,挨個字符比較,如果匹配就繼續,如果不匹配則father和son同時回退前進的長度,重新計算。java代碼如下:
/**
* 暴力算法
*/
private static int violentMatch(String father, String son) {
int fatherLength = father.length();
int sonLength = son.length();
//i記錄在father中的位置
int i = 0;
//i記錄在son中的位置
int j = 0;
while (i < fatherLength && j < sonLength) {
if (father.charAt(i) == son.charAt(j)) {
//如果父子當前的字符匹配成功,則同時前進以爲,匹配下一個
i++;
j++;
} else {
//匹配不成功,則回退前進的長度
//因爲j每次都是從son的第一位開始,也就是0,所以i-j就是回到這一輪開始的位置,再+1則表示下一個字符(當前輪開始的字符匹配不成功)
i = i - j + 1;
j = 0;
}
}
//如果跳出循環之後,j等於子串的長度(循環進行的條件之一是j < sonLength,匹配成功後j++ 就等於sonLength了)
if (j == sonLength) {
//此時i所在位置是son在father中的最後一個字符的後一位,所以起始位置要減去j,就是son在father中的第一位
return i - j;
}
//沒有匹配成功就返回-1
return -1;
}
3、暴力求解的問題在哪呢?
在於i跟j同步回溯,會造成很多不必要的對比次數。比如father=“abcdef", son="abx",
第一次開始比較的時候,到father的 c 位置就發現不匹配,結束回退再次循環,但是其實人工來看完全不需要從 b 和 c 再開始了,因爲第一次比較已經知道了father 的 b 和 c 與son的 a 是不一樣的,更何況son的 x 在father的前三位壓根就沒有,完全可以從 d 開始。
三、KMP算法之正常邏輯求解(next數組)
(一)、基本算法流程
假設father匹配到 i 位置,son匹配到 j 位置。
-如果j = -1, 或者當前匹配字符成功(father.charAt(i) == son.charAt(j)), 都令i++, j++, 繼續匹配下一個字符。
-如果j != -1, 且當前字符匹配失敗,則令 i 不變,j=next[j], 意思是當前字符匹配失敗,son相對於father向右移動了j - next[j] 位。就比如father="abcabcd", son="abcd", next[]=[-1, 0, 0, 0 ],匹配到father.charAt(3)時,father的字符是a,son的字符是d,i = 3, j = 3,這個時候如果暴力解法,則i = 1, j = 0,而KMP算法則是將 i 不變, j = next[j] = 0,意味着將son右移 j - next[j] = 3。
用代碼表示如下:
/**
* KMP算法
*/
private static int match(String father, String son) {
//通過子串計算next數組
int[] next = getNext(son);
int i = 0, j = 0;
//一直循環到i等於father長度,或者j等於son長度
while (i <= father.length() - 1 && j <= son.length() - 1) {
//-1表示son需要從頭匹配,||後面的語句表示字符相等,後移一位
if (j == -1 || father.charAt(i) == son.charAt(j)) {
i++;
j++;
} else {
//字符不匹配,i不變,j回退到next[j]進行比較,或者說son右移j - next[j]位
j = next[j];
}
if (j > son.length() - 1) {
//匹配成功
return i - son.length();
}
}
return -1;
}
(二)、next數組計算原理
1、尋找前綴後綴最長公共元素長度(這裏看到一篇博客說的比較詳細,直接引用了,鏈接:https://blog.csdn.net/v_july_v/article/details/7041827)
2、然後自己再解釋一下,爲什麼“next 數組考慮的是除當前字符外的最長相同前綴後綴”,見下圖
(三)、next計算代碼
private static int[] getNext(String son) {
//因爲是自己跟自己比較,所以起始狀態j 比 i小1。只是-1不存在,所以下方next[0]設置成了-1
//配合判斷條件j == -1,造成的結果就是i和j一起進一位。如果之前的步驟兩個字符相同,那就是都進一位繼續比較。
// 如果不同,j置爲-1然後自增變成0,i進一位,就開始了多一位字符的運算。
int i = 0, j = -1;
int[] next = new int[son.length()];
next[0] = -1;
while (i < son.length() - 1) {
//以son = "ababd"爲例
//起始的時候j=-1,令i = 1, j = 0,所以next[1] = 0,而且不管是什麼字符串,都會是0,
// 因爲next 數組考慮的是除當前字符外的最長相同前綴後綴。所以i = 1的時候,next數組考慮的只有son[0]這一個字符
//而當i=1了,j=0,son[1]和son[0]不相等,則j就要回退,回退到next[j],此處原理與match時候的j = next[j]的原理很相似。
if (j == -1 || son.charAt(i) == son.charAt(j)) {
i++;
j++;
next[i] = j;
} else {
//son[i]和son[j]不相等,則j就要回退,回退到next[j],此處原理與match時候的j = next[j]的原理很相似。
// 回退到j = 0時就相當於此時的字符與son[0]開始比較,不相等則j=next[0]=-1,緊接着就各自加1,開始多一位字符長度的比較
j = next[j];
}
}
//感興趣的可以再用son="abcdabcdcab"等進行debug觀察計算和回溯過程
return next;
}
四、自述
這周開始在看《大話數據結構》,這算是書中目前爲止遇到的第一個真正意義上的算法吧,就把我難住了,大概看了一天半吧纔有了大概的輪廓,但是感覺細節還不是很清晰,就想着通過寫博客的方式來講述,加深理解。如果大家覺得哪裏講的有問題或者不夠透測,可以留言討論。
邊寫博客邊加深理解,也算是耗費了一些時間,一週就這樣過去了(階段性空閒,程序員都懂的,可別說我工作不飽和好吧……^_^)。本來想再把知乎上看到的另一種解法--確定有限狀態機 也寫在這裏的,但是時間來不及了,畢竟還是要工作的。就準備等週末來寫好了。然後下週再研究KMP算法的改進算法--nextval數組方式,以及Sunday算法。