字符串 - KMP算法
字符串算法中,字符串匹配是一個非常重要的應用。例如在網頁中查找關鍵詞,其實就是在對字符串匹配,也就是看一個主字符串中是否包含了一個子字符串。
而KMP算法在字符串匹配方法中一個很著名並且很聰明的算法,當然也確實比較難理解。甚至於有程序員因為無法理解KMP算法而直接改用暴力匹配。本身自己學算法起步較晚,第一次接觸到KMP算法已經是研究生畢業一年了。雖然帶著研究生的學歷背景,但是剛開始看的時候依然是一臉懵逼。看了很多博主的講解總算是明白了,所以在這篇博客中記錄下來自己的理解,如果能幫助到別人也是萬分榮幸了。本篇博客參考了孤~影~關於KMP算法的講解。
先從暴力匹配方法感受一下字符串匹配的過程。其中i表示匹配過程中主字符串的位置,j表示子字符串的位置。可以看出,j總是從0開始匹配,而i也是從0開始,一步一步向後移。
註意上面的算法,是i和j同時在移動,如果遇到不匹配的,那麽i和j同時回退。這種算法簡單粗暴好理解,但是仍然有改進的地方。
以上圖為例,想象一下是自己手動拿著P去匹配S,假如第一輪沒匹配上,那麽我們很自然想到直接從第一輪跳到第四輪,因為第二輪和第三輪的首字母A明顯就和S中的字符B、C不符。
再來幾個例子感受下這個更快速的移動過程,讓我們試著發現一些規律。
上面三個例子中移動位置其實是比較理想的,假想是我們人為去匹配多半也是這樣比劃的。那麽觀察一下紅框中的字符串,其實存在一定的規律。
其實在這些情況下之所以存在比暴力算法更快的匹配,就是因為子字符串本身就包含了一定的特殊性。我們可以從上圖中看出,子字符串的首和尾其實存在著重復的字符串。更具一般性的,
子字符串P中的,P[0] ~ P[k-1] = P[j-k] ~ P[j-1]。也就是首尾重復字符串,註意也是最大的首尾重復字符串。註意以下一種情況。
以上就是為了說明,假如我們遇到了第j個字符不匹配時,可以直接移動到k,而不需要比較前k-1個字符。
於是現在的重點就在於怎麽求k。
1)next數組是什麽?
next數組記錄了子字符串遇到不匹配時要回退的位置。上述舉的例子恰好是P數組最後一個字符不匹配,但實際上不匹配可能出現在P的任何一個位置。因此我們為長度為n的P字符串建立一個長度也為n的next數組。next[j]表示,當P的第j個字符出現不匹配時,字符匹配子字符串應該移動到的位置,如下圖所示。
具體來說,以"abbaabcab"為例,假設要計算next[6]。那麽對於"abbaab"來說:
頭部有: a ab abb abba abbaa(不包含最後一個字符)
尾部有: b ab aab baab bbaab (不包含第一個字符)
最長首尾相同字符串就是ab,長度為2。那麽next[6] = 2。
2)如何求next數組?
假設k表示當前最大首尾相同字符串的頭部,j指向
- 初始時刻,k指向-1,j指向0的位置。
- 當遇到首尾相同的字符時,k和j都向右移動一位
- 當遇到首尾不同的字符時,k向左回退到next[k]
也就是說,當P[k]不等於P[j]時,k必須要回退到次大的首尾相同字符串,也就是上圖中的黃色塊。
結合代碼可能更清楚。求next數組的代碼:
public static int[] getNext(String ps) { char[] p = ps.toCharArray(); int[] next = new int[p.length]; next[0] = -1; int j = 0; int k = -1; while (j < p.length - 1) { if (k == -1 || p[j] == p[k]) { next[++j] = ++k; } else { k = next[k]; } } return next; }
求得了next數組之後,就可以寫KMP算法了:
public static int KMP(String ts, String ps) { char[] t = ts.toCharArray(); char[] p = ps.toCharArray(); int i = 0; // 主串的位置 int j = 0; // 模式串的位置 int[] next = getNext(ps); while (i < t.length && j < p.length) { if (j == -1 || t[i] == p[j]) { // 當j為-1時,要移動的是i,當然j也要歸0 i++; j++; } else { j = next[j]; // j回到指定位置 } } if (j == p.length) { return i - j; } else { return -1; } }
以上就是KMP算法的原理了,總結一下:
1)KMP算法比暴力匹配改進的地方其實只在於當子字符串本身存在著一定的首尾相同的情況時,可以直接跳到頭部字符串的末端。
2)KMP算法保證了主字符串的位置i不回退,而只回退子字符串的位置j,而且j不需要每次都從0開始。
參考資料: 《算法》第四版
https://www.cnblogs.com/yjiyjige/p/3263858.html
字符串 - KMP算法