KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人們稱它為克努特—莫里斯—普拉特操作(簡稱KMP算法)。KMP算法的核心是利用匹配失敗后的信息,盡量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是通過一個next()函數實現,函數本身包含了模式串的局部匹配信息。KMP算法的時間復雜度O(m+n)。
算法分析說明
問題:查找一個短的字符串在一個長的字符串中的位置(具體點就是:text="The boy has no girlfriend", s="has girlfriend"
;現在判斷在text中是否包含s,如果包含返回在text中對應的下標)。KMP算法就是一種快速的解決方案。
BF算法
BF算法是普通的模式匹配算法,BF算法的思想就是將目標串S的第一個字符與模式串P的第一個字符進行匹配,若相等,則繼續比較S的第二個字符和P的第二個字符;若不相等,則比較S的第二個字符和P的第一個字符,依次比較下去,直到得出最后的匹配結果。時間復雜度為O(n*m)。
Java版本
public static int bfMatch(String text, String s){
int m = s.length();
int n = text.length();
if(m>n){
return -1;
}
int i=0;
while (i<n){
// if(text.substring(i).startsWith(s)){
// return i;
// }
int j = 0;
while (j<m && text.charAt(i) == s.charAt(j)){
j++;
i++;
}
if(j==m){
return i-m;
}
// 回溯
i = i-j+1;
}
return -1;
}
Golang版
func bfMatch(text string, s string) int {
m, n := len(s), len(text)
if m > n {
return -1
}
i :=0
for i<n {
j :=0
// 我們這里默認沒有中文字符
for j < m && text[i]==s[j] {
j++
i++
}
if j==m {
return i-m
}
// 回溯
i = i-j+1
}
return -1
}
KMP算法
KMP算法是一種無回溯的算法;時間和空間復雜度都為為O(m+n)。上面的算法中我們發現其實每次不用在匹配失敗后重新回到前面去,想象一下如果是人腦去匹配會如何做?
我們在匹配失敗后是不會重新回到前面去的,為什么呢?因為我們知道一些已經匹配的,就不用再去弄了。
KMP算法是利用已經部分匹配這個有效信息,保持i指針不回溯,通過修改j指針,讓模式串盡量地移動到有效的位置;也就是要解決當某一個字符與主串不匹配時,我們應該知道j指針要移動到哪?
我們假設重新移動的位置為k,那么要保證移動后前面的一部分是匹配的,那一定是前面長度為k的字符串和j前面的k個字符串是匹配的,這樣才可以保證后面不需要重新在匹配了;下面我們舉個例子來說明。
假設現在模式串匹配到下標為j的位置發現不匹配,此時目標串的下標i不變,現在移動模式串的下標到k開始重新匹配;那么需要滿足如下條件:模式串(0~k-1) = 模式串(j-k~j-1);我們下面來畫個圖進行說明。
也就是說我們只動模式串,如果在i前面的存在匹配的組合,那么一定有一部分是和模式串的一部分匹配的,而k前面的那部分就是匹配的。
算法定義
- next: 為對應模式串的數組,里面保存的值就是前后綴最長的匹配數,也就是在遇到不匹配時j重新移動的開始位置。
- 設模式串為P,則next[j]=k,當且僅當滿足如下條件:P[0 ~ k-1] == P[j-k ~ j-1]
- 通俗地講: next[j]保存了以S[j]為結尾的后綴與模式串前綴的最長匹配數。
這里舉個列子來說明如何計算next[j]的值。
對應下標:0 1 2 3 4 5 6 7 8 9
模式串為:a b c a b c d d e a
next: 0 0 0 0 1 2 3 0 0 0
當j=5時,當前用到的字符串為abcab c
; 它的后綴有:b, ab, cab, bcab, abcab;前綴有:ac, ab, abc, abca, abcab;最后的abcab是無效的,那么前后綴中匹配的最大長度是2。因為數組的下標是從0開始,因此直接使用最長匹配數正好移動位置就是已經匹配過的。
Golang版
主要是求的next的實現,我們通過下面通過2中方式來實現。
直接求解法:挨着去找每一段的前后綴最長匹配串長度;這種方式很好理解就不過多說明了。
func getNext1(p string) []int {
i, j, pLen := 0, 0, len(p)
next := make([]int, pLen)
for ; i<pLen;i++{
if i == 0 {
next[i] = -1
}else if i ==1 {
next[i] = 0
}else {
tmp := i-1
// 前后比較串的長度依次剪短,這樣如果某次循環時,如果串匹配那么就是最長匹配串
for j=tmp; j>0; j--{
// 判斷P[0~j] == P[tmp-j+1, tmp]
s := p[0:j]
e := p[tmp-j+1:i]
if s == e {
next[i] = j
break
}
//if compare(p, j, i, tmp-j+1) {
// next[i] = j
// break
//}
}
if j == 0 {
next[i] = 0
}
}
}
return next
}
func compare(p string, prefixEnd int, suffixEnd int, suffixStart int) bool {
prefixStart := 0
for ; prefixStart<prefixEnd && suffixStart<suffixEnd; {
if p[prefixStart] != p[suffixStart] {
return false
}
suffixStart++
prefixStart++
}
return true
}
按照遞推的思想求next:這種方式比較抽象,這里進行一些說明,希望可以幫助理解。 其實這種推導的方式就是優化了內層循環中找前后綴最大匹配長度的過程;我們分析上面算法的內層循環查找最大匹配串時,發現前后綴的規則(定義前綴的結束點為k,后綴的結束點為i),其中前綴的起始都是從下標0開始的,而后綴的終點都是i,如果不匹配前后綴都會減1。下面我們分情況來討論:
-
對於每次內層循環都在第一次就匹配成功的情況:那么本次匹配和上一次的匹配的區別是啥,區別就是前后綴就只多了一個字符,我們只需要判斷這一個字符是否相同就可以判斷本次是否匹配了,即P[i] = P[K];同時本次的最大匹配長度就是k+1; 因為我們的后綴是包含了當前i這個位置的字符的,所有求得就是i+1處的移動位置,因此
next[i+1] = k+1
。 -
對於內層循環中需要進行多次才能判斷是否有匹配的情況:這個時候我們換個角度來看這個過程,在這個過程中如果匹配失敗,前后綴長度會減1之后再次比較;我們把這個過程抽象一下是否就是在P[0-i]中找P[0-k],而此時正好匹配到i和k這個位置,現在發現不匹配了,因此需要移動位置重新開始匹配,嘿 這不就是kmp算法中在匹配過程中遇到不匹配時找k的移動位置的情況嗎;因為此時 i>k,那么next[k]的最長匹配串已經求出來了,也就是說現在已經知道k應該重新開始的位置了即
k=next[k]
。
func getNext(p string) []int {
i, k, sLen := 0, -1, len(p)
next := make([]int, sLen)
next[0] = -1
for i < sLen {
if k == -1 || p[i] == p[k] { // P[i]==P[k]
k++
i++
if i < sLen {
next[i] = k
}
}else { // P[i]!=P[k]
k = next[k]
}
}
return next
}
kmp比較算法:這里只需要遍歷目標串text即可,匹配失敗時直接從next中獲取模式串的下標變化情況。時間復雜度為O(n)
func kmp(text string, p string, next []int) int {
i, j, tLen, pLen := 0, 0, len(text), len(p)
for i < tLen {
if j == -1 || text[i] == p[j] {
i++
j++
}else {
// 匹配失敗,移動模式串的位置
j = next[j]
}
if j == pLen {
// 匹配成功
return i-pLen
}
}
return -1
}
Java版
直接求解法
public int[] getNext(String p){
int len = p.length();
int j = 0;
int[] next = new int[len];
for (int i=0; i<len; i++){
if(i==0){
next[i] = -1;
}else if(i==1){
next[i] = 0;
}else {
int tmp = i-1;
for (j=tmp; j>0; j--){
if (p.substring(tmp-j+1, i).startsWith(p.substring(0, j))) {
next[i] = j;
break;
}
}
if (j == 0){
next[i] = 0;
}
}
}
return next;
}
按照遞推的思想求next
public int[] getNext(String p){
int i=0;
int k=-1;
int[] next = new int[p.length()];
next[0] = -1;
while (i<p.length()) {
if(k==-1 || p.charAt(i)==p.charAt(k)){
k++;
i++;
if(i<p.length()){
next[i] = k;
}
}else {
k = next[k];
}
}
return next;
}
kmp比較算法
public int kmp(String text, String p){
// 求next
int[] next = getNext(p);
int k = 0;
int i = 0;
while (i<text.length()){
if ( k==-1 || text.charAt(i)==p.charAt(k)){
i++;
k++;
}else {
k = next[k];
}
if (k==p.length()){
return i-k;
}
}
return -1;
}