0、廢話
一直ym傳說中的kmp算法能以最壞線性的時間復雜度搞定字符串匹配,
開始動手看才知道kmp中的K居然是Donald.E.Knuth,《計算機程序設計藝術》的作者。
好吧,繼續ym……
1、傳統的字符串匹配算法
/* * 從s中第sIndex位置開始匹配p * 若匹配成功,返回s中模式串p的起始index * 若匹配失敗,返回-1 */ int index(const std::string &s, const std::string &p, const int sIndex = 0) { int i = sIndex, j = 0; if (s.length() < 1 || p.length() < 1 || sIndex < 0) { return -1; } while (i != s.length() && j != p.length()) { if (s[i] == p[j]) { ++i; ++j; } else { i = i - j + 1; j = 0; } } return j == p.length() ? i - j: -1; }
2、傳統字符串匹配算法的性能問題
用模式串P去匹配字符串S,在i=6,j=4時發生失配:
i=6
S: a b a b c a d c a c b a b
P: a b c a c
j=4
此時,按照傳統算法,應當將P的第 1 個字符 a(j=0) 滑動到與S中第4個字符 b(i=3) 對齊再進行匹配:
i=3
S: a b a b c a a d a c b a b
P: a b c a c
j=0
這個過程中,對字符串S的訪問發生了“回朔”(從 i=6 移回到 i=3)。
我們不希望發生這樣的回朔,而是試圖通過盡可能的“向右滑動”模式串P,讓P中index為 j 的字符對齊到S中 i=5 的字符,然后試圖匹配S中 i=6 的字符與P中index為 j+1 的字符。
在這個測試用例中,我們直接將P向右滑動3個字符,使S中 i=5 的字符與P中 j=0 的字符對齊,再匹配S中 i=6 的字符與P中 j=1 的字符。
i=6
S: a b a b c a d c a c b a b
P: a b c a c
j=0
3、kmp算法的一般性討論
下面討論在一般性的情況下,如何實現在“不回朔”訪問S、僅依靠“滑動”P的前提下實現字符串匹配,即“kmp算法”。
i=6
S: a b a b c a d c a c b a b
P: a b c a c
k=1
i=6
S: a b a b c a d c a c b a b
P: a b c a c
j=4
對於任意的S和P,當S中index為 i 的字符和P中index為 j 的字符失配時,我們假定應當滑動P使其index為 k 的字符與S中index為 i 的字符“對齊”並繼續比較。
那么,這個 k 是多少?
我們知道,所謂的對齊,就是要讓S和P滿足以下條件(上圖中的藍色字符):
……(1)
另一方面,在失配時我們已經有了一些部分匹配結果(上圖中的綠色字符):
……(2)
由(1)、(2)可以得到:
……(3)
即如下圖所示效果:
定義next[j]=k,k表示當模式串P中index為 j 的字符與主串S中index為 i 的字符發生失配時,應將P中index為 k 的字符繼續與主串S中index為 i 的字符比較。
……(4)
按上述定義給出next數組的一個例子:
j 0 1 2 3 4 5 6 7
P a b a a b c a c
next[j] -1 0 0 1 1 2 0 1
在已知next數組的前提下,字符串匹配的步驟如下:
i 和 j 分別表示在主串S和模式串P中當前正待比較的字符的index,i 的初始值為sIndex,j 的初始值為0。
在匹配過程中的每一次循環,若,i 和 j 分別增 1,
else,j 退回到 next[j]的位置,此時下一次循環是與
相比較。
4、kmp算法的實現
在已知next函數的前提下,根據上面的步驟,kmp算法的實現如下:
int kmp(const std::string& s, const std::string& p, const int sIndex = 0) { std::vector<int>next(p.size()); getNext(p, next);//獲取next數組,保存到vector中 int i = sIndex, j = 0; while(i != s.length() && j != p.length()) { if (j == -1 || s[i] == p[j]) { ++i; ++j; } else { j = next[j]; } } return j == p.length() ? i - j: -1; }
ok,下面的問題是怎么求模式串 P 的next數組。
next數組的初始條件是next[0] = -1,設next[j] = k,則有:
那么,next[j+1]有兩種情況:
①,則有:
此時next[j+1] = next[j] + 1 = k + 1
②, 如圖所示:
此時需要將P向右滑動之后繼續比較P中index為 j 的字符與index為 next[k] 的字符:
值得注意的是,上面的“向右滑動”本身就是一個kmp在失配情況下的滑動過程,將這個過程看 P 的自我匹配,則有:
如果,則next[j+1] = next[k] + 1;
否則,繼續將 P 向右滑動,直至匹配成功,或者不存在這樣的匹配,此時next[j+1] = 0。
getNext函數的實現如下:
void getNext(const std::string &p, std::vector<int> &next) { next.resize(p.size()); next[0] = -1; int i = 0, j = -1; while (i != p.size() - 1) { //這里注意,i==0的時候實際上求的是next[1]的值,以此類推 if (j == -1 || p[i] == p[j]) { ++i; ++j; next[i] = j; } else { j = next[j]; } } }
至此,一個完整的kmp已經實現。
5、getNext函數的進一步優化
注意到,上面的getNext函數還存在可以優化的地方,比如:
i=3
S: a a a b a a a a b
P: a a a a b
j=3
此時,i=3、j=3時發生失配,next[3]=2,此時還需要進行 3 次比較:
i=3, j=2;
i=3, j=1;
i=3, j=0。
而實際上,因為i=3, j=3時就已經知道a!=b,而之后的三次依舊是拿 a 和 b 比較,因此這三次比較都是多余的。
此時應當直接將P向右滑動4個字符,進行 i=4, j=0的比較。
一般而言,在getNext函數中,next[i]=j,也就是說當p[i]與S中某個字符匹配失敗的時候,用p[j]繼續與S中的這個字符比較。
如果p[i]==p[j],那么這次比較是多余的(如同上面的例子),此時應該直接使next[i]=next[j]。
完整的實現代碼如下:
void getNextUpdate(const std::string& p, std::vector<int>& next) { next.resize(p.size()); next[0] = -1; int i = 0, j = -1; while (i != p.size() - 1) { //這里注意,i==0的時候實際上求的是nextVector[1]的值,以此類推 if (j == -1 || p[i] == p[j]) { ++i; ++j; //update //next[i] = j; //注意這里是++i和++j之后的p[i]、p[j] next[i] = p[i] != p[j] ? j : next[j]; } else { j = next[j]; } } }
對應的,只需要在kmp算法中將 getNext(p, next); 替換成 getNextUpdate(p, next); 即可。
6、時間復雜度分析
下面以getNext函數為例,分析kmp算法的時間復雜度。
1 void getNext(const std::string& p, std::vector<int>& next) 2 { 3 next.resize(p.size()); 4 next[0] = -1; 5 6 int i = 0, j = -1; 7 8 while (i != p.size() - 1) 9 { 10 if (j == -1 || p[i] == p[j]) 11 { 12 ++i; 13 ++j; 14 next[i] = j; 15 } 16 else 17 { 18 j = next[j]; 19 } 20 } 21 }
假定p.size()為m,分析其時間復雜度的困惑在於,在while里面不是每次循環都執行 ++i 操作,所以整個while的執行次數不一定為m。
換個角度,注意到在每次循環中,無論 if 還是 else 都會修改 j 的值且每次循環僅對 j 進行一次修改,所以在整個while中 j 被修改的次數即為getNext函數的時間復雜度。
每次成功匹配時,++i; ++j; , 由於 ++i 最多執行 m-1 次,故++j也最多執行 m-1 次,即 j 最多增加m-1次;
對應的,只有在 j=next[j]; 處 j 的值一定會變小,由於 j 最多增加m-1次,故 j 最多減小m-1次。
綜上所述,getNext函數的時間復雜度為O(m),若帶匹配串S的長度為n,則kmp函數的時間負責度為O(m+n)。
7、kmp的應用優勢
①快,O(m+n)的線性最壞時間復雜度;
②無需回朔訪問待匹配字符串S,所以對處理從外設輸入的龐大文件很有效,可以邊讀入邊匹配。