數據結構20:KMP算法(快速模式匹配算法)詳解
阿新 • • 發佈:2018-05-14
sni 實現 inf 基礎 二次 是否 升級 有用 簡化
"KMP"算法相比於"BF"算法,優勢在於:
故,"KMP"算法稱為“快速模式匹配算法”。
在模式串和主串匹配時,各有一個指針指向當前進行匹配的字符(主串中是指針 i ,模式串中是指針 j ),在保證 i 指針不回溯的前提下,如果想實現功能,就只能讓 j 指針回溯。
j 指針回溯的距離,就相當於模式串向右移動的距離。 j 指針回溯的越多,說明模式串向右移動的距離越長。 計算模式串向右移動的距離,就可以轉化成:當某字符匹配失敗後, j 指針回溯的位置。
對於一個給定的模式串,其中每個字符都有可能會遇到匹配失敗,這時對應的 j 指針都需要回溯,具體回溯的位置其實還是由模式串本身來決定的,和主串沒有關系。
模式串中的每個字符所對應 j 指針回溯的位置,可以通過算法得出,得到的結果相應地存儲在一個數組中(默認數組名為 next )。
計算方法是:對於模式串中的某一字符來說,提取它前面的字符串,分別從字符串的兩端查看連續相同的字符串的個數,在其基礎上 +1 ,結果就是該字符對應的值。 每個模式串的第一個字符對應的值為 0 ,第二個字符對應的值為 1 。 例如:求模式串 “abcabac” 的 next 。前兩個字符對應的 0 和 1 是固定的。
對於字符 ‘c’ 來說,提取字符串 “ab” ,‘a’ 和 ‘b’ 不相等,相同的字符串的個數為 0 ,0 + 1 = 1 ,所以 ‘c’ 對應的 next 值為 1 ;
第四個字符 ‘a’ ,提取 “abc” ,從首先 ‘a’ 和 ‘c’ 就不相等,相同的個數為 0 ,0 + 1 = 1 ,所以,‘a’ 對應的 next 值為 1 ;
第五個字符 ‘b’ ,提取 “abca” ,第一個 ‘a’ 和最後一個 ‘a’ 相同,相同個數為 1 ,1 + 1 = 2 ,所以,‘b’ 對應的 next 值為 2 ;
第六個字符 ‘a’ ,提取 “abcab” ,前兩個字符 “ab” 和最後兩個 “ab” 相同,相同個數為 2 ,2 + 1 = 3 ,所以,‘a’ 對應的 next 值為 3 ;
最後一個字符 ‘c’ ,提取 “abcaba” ,第一個字符 ‘a’ 和最後一個 ‘a’ 相同,相同個數為 1 ,1 + 1 = 2 ,所以 ‘c’ 對應的 next 值為 2 ;
所以,字符串 “abcabac” 對應的 next 數組中的值為(0,1,1,1,2,3,2)。
上邊求值過程中,每次都需要判斷字符串頭部和尾部相同字符的個數,而在編寫算法實現時,對於某個字符來說,可以借用前一個字符的判斷結果,計算當前字符對應的 next 值。
具體的算法如下:
模式串T為(下標從1開始):“abcabac”
next數組(下標從1開始): 01
第三個字符 ‘c’ :由於前一個字符 ‘b’ 的 next 值為 1 ,取 T[1] = ‘a’ 和 ‘b’ 相比較,不相等,繼續;由於 next[1] = 0,結束。 ‘c’ 對應的 next 值為1;(只要循環到 next[1] = 0 ,該字符的 next 值都為 1 )
模式串T為: “abcabac”
next數組(下標從1開始):011
第四個字符 ’a‘ :由於前一個字符 ‘c’ 的 next 值為 1 ,取 T[1] = ‘a’ 和 ‘c’ 相比較,不相等,繼續;由於 next[1] = 0 ,結束。‘a’ 對應的 next 值為 1 ;
模式串T為: “abcabac”
next數組(下標從1開始):0111
第五個字符 ’b’ :由於前一個字符 ‘a’ 的 next 值為 1 ,取 T[1] = ‘a’ 和 ‘a’ 相比較,相等,結束。 ‘b’ 對應的 next 值為:1(前一個字符 ‘a’ 的 next 值) + 1 = 2 ;
模式串T為: “abcabac”
next數組(下標從1開始):01112
第六個字符 ‘a’ :由於前一個字符 ‘b’ 的 next 值為 2,取 T[2] = ‘b’ 和 ‘b’ 相比較,相等,所以結束。‘a’ 對應的 next 值為:2 (前一個字符 ‘b’ 的 next 值) + 1 = 3 ;
模式串T為: “abcabac”
next數組(下標從1開始):011123
第七個字符 ‘c’ :由於前一個字符 ‘a’ 的 next 值為 3 ,取 T[3] = ‘c’ 和 ‘a’ 相比較,不相等,繼續;由於 next[3] = 1 ,所以取 T[1] = ‘a’ 和 ‘a’ 比較,相等,結束。‘a’ 對應的 next 值為:1 ( next[3] 的值) + 1 = 2 ;
模式串T為: “abcabac”
next數組(下標從1開始):0111232
算法實現:
第一次匹配:
匹配失敗,i 指針不動,j = 1(字符‘c’的next值);
第二次匹配:
相等,繼續,直到:
匹配失敗,i 不動,j = 2 ( j 指向的字符 ‘c’ 的 next 值);
第三次匹配:
相等,i 和 j 後移,最終匹配成功。 使用普通算法,需要匹配 6 次;而使用 KMP 算法,則只匹配 3 次。 實現代碼:
例如: 模式串T:a b c a c
next :0 1 1 1 2 在模式串“abcac”中,有兩個字符 ‘a’,我們假設第一個為 a1,第二個為 a2。在程序匹配過程中,如果 j 指針指向 a2 時匹配失敗,那麽此時,主串中的 i 指針不動,j 指針指向 a1 ,很明顯,由於 a1==a2,而 a2!=S[i],所以 a1 也肯定不等於 S[i]。
為了避免不必要的判斷,需要對 next 數組進行精簡,對於“abcac”這個模式串來說,由於 T[4] == T[next[4]] ,所以,可以將next數組改為:
模式串T:a b c a c
next :0 1 1 0 2 這樣簡化,如果匹配過程中由於 a2 匹配失敗,那麽也不用再判斷 a1 是否匹配,因為肯定不可能,所以直接繞過 a1,進行下一步。
實現代碼:
例如:精簡前為 next1,精簡後為 next2: 模式串:a a a a a a a b
next1:0 1 2 3 4 5 6 7
next2:0 0 0 0 0 0 0 7
通過上一節的介紹,學習了串的普通模式匹配算法,大體思路是:模式串從主串的第一個字符開始匹配,每匹配失敗,主串中記錄匹配進度的指針 i 都要進行 i-j+1 的回退操作(這個過程稱為“指針回溯”),同時模式串向後移動一個字符的位置。一次次的循環,直到匹配成功或者程序結束。
"KMP"算法相比於"BF"算法,優勢在於:
- 在保證指針 i 不回溯的前提下,當匹配失敗時,讓模式串向右移動最大的距離;
- 並且可以在
O(n+m)
的時間數量級上完成對串的模式匹配操作;
故,"KMP"算法稱為“快速模式匹配算法”。
模式串向右移動距離的計算
j 指針回溯的距離,就相當於模式串向右移動的距離。 j 指針回溯的越多,說明模式串向右移動的距離越長。 計算模式串向右移動的距離,就可以轉化成:當某字符匹配失敗後, j 指針回溯的位置。
對於一個給定的模式串,其中每個字符都有可能會遇到匹配失敗,這時對應的 j 指針都需要回溯,具體回溯的位置其實還是由模式串本身來決定的,和主串沒有關系。
模式串中的每個字符所對應 j 指針回溯的位置,可以通過算法得出,得到的結果相應地存儲在一個數組中(默認數組名為 next )。
計算方法是:對於模式串中的某一字符來說,提取它前面的字符串,分別從字符串的兩端查看連續相同的字符串的個數,在其基礎上 +1 ,結果就是該字符對應的值。 每個模式串的第一個字符對應的值為 0 ,第二個字符對應的值為 1 。 例如:求模式串 “abcabac” 的 next 。前兩個字符對應的 0 和 1 是固定的。
對於字符 ‘c’ 來說,提取字符串 “ab” ,‘a’ 和 ‘b’ 不相等,相同的字符串的個數為 0 ,0 + 1 = 1 ,所以 ‘c’ 對應的 next 值為 1 ;
第四個字符 ‘a’ ,提取 “abc” ,從首先 ‘a’ 和 ‘c’ 就不相等,相同的個數為 0 ,0 + 1 = 1 ,所以,‘a’ 對應的 next 值為 1 ;
第五個字符 ‘b’ ,提取 “abca” ,第一個 ‘a’ 和最後一個 ‘a’ 相同,相同個數為 1 ,1 + 1 = 2 ,所以,‘b’ 對應的 next 值為 2 ;
第六個字符 ‘a’ ,提取 “abcab” ,前兩個字符 “ab” 和最後兩個 “ab” 相同,相同個數為 2 ,2 + 1 = 3 ,所以,‘a’ 對應的 next 值為 3 ;
最後一個字符 ‘c’ ,提取 “abcaba” ,第一個字符 ‘a’ 和最後一個 ‘a’ 相同,相同個數為 1 ,1 + 1 = 2 ,所以 ‘c’ 對應的 next 值為 2 ;
所以,字符串 “abcabac” 對應的 next 數組中的值為(0,1,1,1,2,3,2)。
上邊求值過程中,每次都需要判斷字符串頭部和尾部相同字符的個數,而在編寫算法實現時,對於某個字符來說,可以借用前一個字符的判斷結果,計算當前字符對應的 next 值。
具體的算法如下:
模式串T為(下標從1開始):“abcabac”
next數組(下標從1開始): 01
第三個字符 ‘c’ :由於前一個字符 ‘b’ 的 next 值為 1 ,取 T[1] = ‘a’ 和 ‘b’ 相比較,不相等,繼續;由於 next[1] = 0,結束。 ‘c’ 對應的 next 值為1;(只要循環到 next[1] = 0 ,該字符的 next 值都為 1 )
模式串T為: “abcabac”
next數組(下標從1開始):011
第四個字符 ’a‘ :由於前一個字符 ‘c’ 的 next 值為 1 ,取 T[1] = ‘a’ 和 ‘c’ 相比較,不相等,繼續;由於 next[1] = 0 ,結束。‘a’ 對應的 next 值為 1 ;
模式串T為: “abcabac”
next數組(下標從1開始):0111
第五個字符 ’b’ :由於前一個字符 ‘a’ 的 next 值為 1 ,取 T[1] = ‘a’ 和 ‘a’ 相比較,相等,結束。 ‘b’ 對應的 next 值為:1(前一個字符 ‘a’ 的 next 值) + 1 = 2 ;
模式串T為: “abcabac”
next數組(下標從1開始):01112
第六個字符 ‘a’ :由於前一個字符 ‘b’ 的 next 值為 2,取 T[2] = ‘b’ 和 ‘b’ 相比較,相等,所以結束。‘a’ 對應的 next 值為:2 (前一個字符 ‘b’ 的 next 值) + 1 = 3 ;
模式串T為: “abcabac”
next數組(下標從1開始):011123
第七個字符 ‘c’ :由於前一個字符 ‘a’ 的 next 值為 3 ,取 T[3] = ‘c’ 和 ‘a’ 相比較,不相等,繼續;由於 next[3] = 1 ,所以取 T[1] = ‘a’ 和 ‘a’ 比較,相等,結束。‘a’ 對應的 next 值為:1 ( next[3] 的值) + 1 = 2 ;
模式串T為: “abcabac”
next數組(下標從1開始):0111232
算法實現:
#include <stdio.h> #include <string.h>
void Next(char *T, int *next)
{ int i = 1; next[1] = 0; int j = 0; while (i<strlen(T))
{ if (j==0 || T[i-1]==T[j-1])
{ i++; j++; next[i] = j; }
else
{ j = next[j]; } } }
註意:在此程序中,next 數組使用的下標初始值為 1 ,next[0] 沒有用到(也可以存放 next 數組的長度)。而串的存儲是從數組的下標 0 開始的,所以程序中為 T[i-1] 和 T[j-1]。
基於next的KMP算法的實現
先看一下 KMP 算法運行流程(假設主串:ababcabcacbab,模式串:abcac)。第一次匹配:
匹配失敗,i 指針不動,j = 1(字符‘c’的next值);
第二次匹配:
相等,繼續,直到:
匹配失敗,i 不動,j = 2 ( j 指向的字符 ‘c’ 的 next 值);
第三次匹配:
相等,i 和 j 後移,最終匹配成功。 使用普通算法,需要匹配 6 次;而使用 KMP 算法,則只匹配 3 次。 實現代碼:
int KMP(char *S, char *T)
{ int next[10]; Next(T, next); //根據模式串T,初始化next數組 int i = 1; int j = 1; while (i<=strlen(S) && j<=strlen(T))
{ //j==0:代表模式串的第一個字符就和指針i指向的字符不相等;S[i-1]==T[j-1],如果對應位置字符相等,兩種情況下,指向當前測試的兩個指針下標i和j都向後移 if (j==0 || S[i-1]==T[j-1])
{ i++; j++; } else
{ j=next[j];//如果測試的兩個字符不相等,i不動,j變為當前測試字符串的next值 } } if (j>strlen(T))
{
//如果條件為真,說明匹配成功 return i-(int)strlen(T); } return -1; }
KMP算法完整代碼
#include <stdio.h> #include <string.h>
void Next(char *T, int *next)
{ int i = 1; next[1] = 0; int j = 0; while (i<strlen(T))
{ if (j==0 || T[i-1]==T[j-1])
{ i++; j++; next[i] = j; }
else
{ j = next[j]; } } }
int KMP(char *S, char *T)
{ int next[10]; Next(T, next); //根據模式串T,初始化next數組 int i = 1; int j = 1; while (i<=strlen(S)&&j<=strlen(T))
{ //j==0:代表模式串的第一個字符就和當前測試的字符不相等;S[i-1]==T[j-1],如果對應位置字符相等,兩種情況下,指向當前測試的兩個指針下標i和j都向後移 if (j==0 || S[i-1]==T[j-1])
{ i++; j++; } else
{ j = next[j];//如果測試的兩個字符不相等,i不動,j變為當前測試字符串的next值 } } if (j>strlen(T))
{
//如果條件為真,說明匹配成功 return i-(int)strlen(T); }
return -1; }
int main()
{ int i = KMP("ababcabcacbab", "abcac"); printf("%d", i);
return 0; }
運行結果: 6
升級版的next
註意:KMP 算法的關鍵在於 next 數組的確定,其實對於上邊的KMP算法中的next數組,不是最精簡的,還可以簡化。例如: 模式串T:a b c a c
next :0 1 1 1 2 在模式串“abcac”中,有兩個字符 ‘a’,我們假設第一個為 a1,第二個為 a2。在程序匹配過程中,如果 j 指針指向 a2 時匹配失敗,那麽此時,主串中的 i 指針不動,j 指針指向 a1 ,很明顯,由於 a1==a2,而 a2!=S[i],所以 a1 也肯定不等於 S[i]。
為了避免不必要的判斷,需要對 next 數組進行精簡,對於“abcac”這個模式串來說,由於 T[4] == T[next[4]] ,所以,可以將next數組改為:
模式串T:a b c a c
next :0 1 1 0 2 這樣簡化,如果匹配過程中由於 a2 匹配失敗,那麽也不用再判斷 a1 是否匹配,因為肯定不可能,所以直接繞過 a1,進行下一步。
實現代碼:
void Next(char *T, int *next)
{ int i = 1; next[1] = 0; int j = 0; while (i<strlen(T))
{ if (j==0 || T[i-1]==T[j-1])
{ i++; j++; if (T[i-1] != T[j-1])
{ next[i] = j; } else
{ next[i] = next[j]; } }
else
{ j = next[j]; } } }
使用精簡過後的 next 數組在解決例如模式串為“aaaaaaab”這類的問題上,會減少很多不必要的判斷次數,提高了KMP算法的效率。
例如:精簡前為 next1,精簡後為 next2: 模式串:a a a a a a a b
next1:0 1 2 3 4 5 6 7
next2:0 0 0 0 0 0 0 7
總結
KMP 算法,之所以比 BF 算法快的根本原因在於:KMP 算法其實也和 BF 算法一樣,都是從主串開頭開始匹配,但是在匹配過程中,KMP算法記錄了一些必要的信息。根據這些信息,在後續的匹配過程中,跳過了一些無意義的匹配過程。數據結構20:KMP算法(快速模式匹配算法)詳解