字符串匹配 -- KMP算法
參考資料
1 數據結構( C 語言版)
概述
在前面的文章
樸素字符串匹配、
Rabin-Karp算法中,對有關字符串匹配問題做了相關的介紹。其中
樸素字符串匹配效率較差,其時間複雜度爲
O ( ( n - m + 1 ) m ) ,當問題規模較大時,該算法的劣勢會變得更加明顯;
Rabin-Karp算法需要一個預處理,其時間爲 O ( m ) ,匹配的最壞時間複雜度爲 O ( ( n - m + 1 ) m ) ,但在平均情況和實際情況下,其效果還是不錯的,所以應用還是很廣的。
事實上還有一種字符串匹配算法 -- KMP算法 ( 有人將此算法戲稱爲看mao片算法,呵呵!!無力吐槽了都!! ),其效率更高。該算法也需要對模式進行預處理,其時間複雜度爲 O ( m ),匹配的時間複雜度爲 O ( n ) 。該算法是由Knuth、Morris、Pratt三人設計的線性時間字符串匹配算法。在此不得不佩服這三位大牛,該算法設計的實在巧妙,代碼簡短精巧,爲了把這個算法看懂還要反覆思考琢磨,花了我不少時間。
算法思想
KMP算法與以往的字符串匹配算法不同的一點是:每當一趟匹配過程中出現比較不匹配時,不需要對字符串進行回溯,而是利用已經得到的“部分匹配”的結果將模式向右滑動儘可能遠的一段距離後,繼續進行比較。
該算法一個最重要的就是對模式進行預處理,也就是通過預處理得到一個數組 next[ ] ,該數組用來說明檔模式中第 j 個字符與主串中相應字符“失配”之後,在模式中需要重新和主串中該字符進行比較的字符的位置。下面將給出求 next 數組的方法。
KMP算法的匹配過程
有一個字符串 str = "acabaabaabcac " ,模式 p = "abaabcac" 。模式 p 的 next 數組已得到,如下圖
得到模式 P 的 next 數組之後,就可以對字符串進行匹配了。
匹配過程
我們用兩個指針i和j分別指向 str 和 P 。在匹配之前,我們需要知道 next 大概要完成什麼樣的工作。我們根據模式
P 的特點,
1 字符串 str 與模式 P 進行匹配,str[ 1 ] != P[ 1 ] ,失配。此時我來做些說明。對於一般情況,當失配的時候,str[ i ] ! = P[ j ],說明 str[ i - j .. i-1] = P[ 0 .. j-1 ] 。
此時,字符串 str 不動,查找模式 P 的 next 數組。next[ 1 ] = 0 ,於是模式從 P[ next [ 1 ] ] 開始與str[ 1 ] 進行比較。
2 此時從 P[ next [ 1 ] ] 也就是 P [ 0 ] 開始與str[ 1 ] 進行比較,失配,此時j = next[ 0] = -1 ,i 和 j 同時都加1,即 i = 2 ,j = 0 ,分別從 str [ 2 ] 和 P [0 ] 開始比較
3 當從 str [ 2 ] 和 P [0 ] 開始比較,一直匹配直到 i = 7 和 j = 5 時失配。此時查找 next[ 5 ] = 2 ,說明 模式 P 在 0~1 與3~ 4 的字符串相等。同時 str[ 3] ~ str[4] = str[6] ~ str[7] 。P [ 0 ] ~ P [ 1 ] 等於 str [ 3 ] ~ str [ 4 ] ,也等於 str [ 6 ] ~ str[ 7 ] ,所以模式在比較的時候 str[
6 ] ~ str[ 7 ] 就不需要比較了,直接從 j = next[5] = 2 處與 str[ 7 ] 進行比較即可。
4 匹配成功!!
next數組
從上面的匹配過程中,可以看到模式右移是根據 next [ j ] 來決定應該要右移多少。那麼next數組是怎麼得到的呢。
我們看一下求next數組的代碼
void get_next( const char* s , int len , int *next)
{
int i = 0;
next[0] = -1;
int j = -1;
while(i < len-1)
{
if( j == -1 || s[i] == s[j]) //j=-1:模式開始位置 ;當s[i] = s[j] :模式將繼續往前
{
++i;
++j;
if( s[i] != s[j]) //不相等,則到此結束
next[i] = j;
else
next[i] = next[j];
}
else //當
j = next[j];
}
}
根據該代碼,我們可以大致得到以下步驟
1 初始化。 i = 0 , j = -1 next[ 0 ] = -1
2 執行循環。
3 滿足 j == -1 或 s[ i ] = s[ j ] 時,i ,j 同時加1 ,之後判斷s[ i ] 是否 s[ j ] 。若不等,則next [ i ] = j ;否則 next [ i ] = next [ j ]
4 若不滿足 j == -1 或 s[ i ] = s[ j ] ,則 j 回溯,j = next [ j ]
next 求解過程
1 模式p = "abaabcac" 。初始化: i = 0 , j = -1 , next [ 0 ] = -1
2 j == -1 ,++i ,++j 則 i= 1,j = 0。P[1] != P[0] 於是 next [1 ] = 0
3 P[1] != P[0] ,執行else 語句,j = next [ 0] = -1 。此時 i = 1 ,j = -1,進入循環之後,i = 2,j= 0 。P[2 ] = P[ 0 ] ,所以 next [ 2 ] = next [ 0] = -1
4 此時i = 2 , j = 0。進入循環,P[2 ] = P[ 0 ],i = 3,j = 1,P[ 3 ] != P [ 1 ] ,next[3] = 1。
5 此時 i = 3 , j = 1。進入循環,P[ 3 ] != P [ 1 ] ,於是執行else 語句,j = next [1 ] = 0。再次比較 P[ 3 ] = P [ 0 ] ,於是 i = 4 , j = 1。P[ 4 ] = P [ 1 ] ,next [ 4 ] = next [ 1 ] = 0
6 此時 i = 4 , j = 1,進入循環,P[ 4 ] = P [ 1 ] ,i= 5,j = 2 ,P[ 5 ] != P [ 2 ] ,next [ 5 ] = 2
7 此時 i = 5 , j = 2 ,進入循環,P[ 5 ] != P [ 2 ] ,執行else 語句,j = next [2 ] = -1。於是i = 6 , j = 0。P[ 6 ] = P [ 0 ] ,則next [ 6 ] = next[0] = -1
8 此時 i = 6, j = 0,進入循環,P[ 6 ] = P [ 0 ] ,i = 7 , j = 1 。P[ 7 ] != P [ 1 ] ,於是 next [ 7 ] = j = 1.
到此,整個求解 next 數組完成了。
KMP代碼實現
#include <iostream>
using namespace std;
void get_next( const char* s , int len , int *next)
{
int i = 0;
next[0] = -1;
int j = -1;
while(i < len-1)
{
if( j == -1 || s[i] == s[j]) //j=-1:模式開始位置 s[i] = s[j] :模式將繼續往前
{
++i;
++j;
if( s[i] != s[j])
next[i] = j;
else
next[i] = next[j];
}
else //當
j = next[j];
}
}
int kmp( const char* s, int slen, const char* p, int plen, const int* next, int pos)
{
int i = pos;
int j = 0;
while( i < slen && j < plen)
{
if( j == -1 || s[i] == p[j]) // s[i] == p[j] : 匹配則繼續向前,s和p同時向前 j=-1表示從模式開頭進行匹配
{
++i;
++j;
}
else //不匹配則模式回溯
j = next[j];
}
if( j >= plen)
return i-plen;
else
return -1;
}
int main()
{
char *str = "acabaabaabcacaabc";
char *p = "abaabcac";
int next[8] ={0};
get_next(p,8,next);
int pos = kmp(str,17,p,8,next,0);
cout << "pos:" << pos << endl;
for(int i = 0 ; i < 8 ; i++)
cout << next[i] << ' ' ;
cout << endl;
return 0;
}
轉載該文章請註明作者和出處 ,請勿用於任何商業用途