1. KMP算法
KMP算法是一種保證線性時間的字符串查找算法,由Knuth、Morris和Pratt三位大神發明,而算法取自這三人名字的首字母,因而得名KMP算法。
那發明這樣的字符串查找算法又有什麼用?在當時計算機本身非常昂貴,計算資源更是極其稀缺,而僅僅進行大文本字符查找的響應時間就很長,沒法充分利用計算資源。計算機可是拿來算更有意義的事的,光爲了找個文本就得浪費這麼多時間,不行啊,這得優化啊。1970年,S.Cook在理論上證明了一個某種特定類型抽象計算機理論。這個理論暗示了一種在最壞情況下時也只是與M+N成正比的解決子字符串查找問題的算法。D.E.Knuth和V.R.Pratt改進了Cook證明定理的框架,並提煉爲一個相對簡單而使用的算法,算法最終在1976年發表。
首先一個例子,這裏使用暴力算法進行求解(即每次查找失敗時,移動一個位置,一直查找,直到找到完全匹配的字符):其中,文本txt[0:9]=“AAAAAAAAAB”,查找的字符pat[0:4]=“AAAAB”。
- i=0時, txt[0:3]=pat[0:3],而txt[4]≠pat[4],匹配失敗
- i=1時, txt[1:4]=pat[0:3],而txt[5]≠pat[4],匹配失敗
- ...
- i=4時, txt[4:7]=pat[0:3],而txt[8]≠pat[4],匹配失敗
- ...
暴力算法在匹配失敗時每次都要回退到開頭,而其實是可以避免回退這麼多,那麼有沒有什麼方法,在模式匹配失敗時進回退一部分呢?
2. KMP原理
KMP算法的主要思想是提前判斷重新開始查找的位置,而這種判斷方式的生成只取決於模式本身。這裏來證明其匹配模式的正確性。
先做以下幾個符號定義
- 待查找文本爲,長度爲
- 模式字符串爲,長度爲
- 爲文本當前所指位置,如
- j爲模式串所指位置,如
假設文本和模式串匹配的起始位置爲,則有,即匹配到當前位置時有。
在匹配過程中,有以下兩種情況
- 當,即大於模式串的長度時,表示文本和模式串完全匹配,這裏匹配結束。
- 當時,表示還在匹配,但發生了失配,接下來主要討論這種情況。
當,但時(即匹配了前j-1個字符,但第j個字符不匹配),假設存在一個最小的偏移量(不存在時另外考慮),能滿足,即能夠讓偏移後的字符能在在失配處儘可能多的匹配文本。就前面這麼短小精悍的一句話,是整個KMP算法的精髓所在,以下舉兩個例子解釋這裏的意思(例子1可能比較抽象,推薦先看例子2)。
例子1: |
i: 1 2 3 4 5 6 7 8 9 10 11 text: b c a b c a a b c a b c pat: c a b c a b c a c |
匹配失敗時, |
首先匹配失敗時,當前的模式串爲"c a b c a b",此時文本爲"c a b c a x", 其中;爲了能儘可能多的和文本“c a b c a x”的後部分內容進行匹配,需要找到最小的偏移量。
爲什麼以這種方式匹配?而且最小偏移量也可能不存在。 原因如下:失配在文本text的"c a b c a x"處,而將模式串pat偏移最小的量,使其再找到一個這樣的位置,滿足再一次模式串和文本的匹配"* * * x"的情況,這時再將文本中的x的值與移動後的模式串進行比較,如下所示。簡單的說,在哪裏跌倒就在哪裏爬起來,只不過需要換一個姿勢。還有一種情況其實是找不到最小偏移量,就將整個模式串大幅向右平移。 |
i: 1 2 3 4 5 6 7 8 9 10 11 text: b c a b c a a b c a b c pat: c a b c a b c a c |
這樣問題就簡化爲如何對於給定模式串,計算其最小偏移量的問題。偏移後字符能滿足這個條件即可進行下一次匹配。 |
例子2: |
看了例子1,可能還沒想明白,即爲什麼非要尋找這麼一個最小偏移量不可,這是論文全文中最關鍵也是最精華的地方。 |
首先做一個很重要的假設:假設存在這麼一個最小偏移量。 對於以下的文本,原先的模式串,那麼對於在新位置的模式串,必須滿足前兩位能和匹配,本質最終還是逃不過在處再次進行決一死戰。我想這裏作者們爲了簡化問題,對失配的情況延後考慮了,避免了text參與偏移量計算造成算法更加複雜,因此只要滿足的條件即可。 |
到了這裏,問題被簡化爲:轉化爲求模式串前綴和後綴能匹配的最大長度。 只要求出這個長度,就能得出需要偏移的量了! Wonderful!接下來就是將這個思路化爲程序即可。 |
3. KMP實例
求模式串前綴和後綴能匹配的最大長度,我使用了以下兩種方式:
- 最直接的理解方式,用遞歸的方式獲取子長度進行匹配,如果不合符則縮小子長度,進行下一次匹配,直到長度爲零,詳情見函數“calcLongestFixed”。
- 論文中作者所說的方式,先計算的情況,再計算。
爲了寫出第三節中的程序,足足花了兩個晚上的時間,來來回回調了N次,就差夢裏也在調了。算法相關爲計算機的關鍵部分,今後繼續加強將算法轉換爲計算機語言的能力!算法下所示,更全面的源碼請見Github。
#include <iostream>
#include <cstring>
#include <vector>
#include <cassert>
using namespace std;
// 我的計算方法
int calcLongestFixed(string strMismatch, string pattern, int max_index) {
if (max_index < 1)
return -1;
int subpos = strMismatch.length() - max_index;
// 從最長的子字符串開始,進行匹配
string subSuffix = strMismatch.substr(subpos, max_index);
string strPrefix = pattern.substr(0, max_index);
int M = subSuffix.length();
string sub_true_suffix = subSuffix.substr(0, M - 1);
string sub_true_prefix = strPrefix.substr(0, M - 1);
char pos_i_char = strPrefix[M - 1]; // 新位置
char pos_j_char = subSuffix[M - 1]; // 原失配處
// 找到pat[1, i - 1] = pat[j - i + 1, j - 1],並滿足
// pat[i] != pat[j]的情況
if (sub_true_suffix.compare(sub_true_prefix) == 0
&& pos_i_char != pos_j_char){
return sub_true_suffix.length();
} else {
return calcLongestFixed(strMismatch, pattern, max_index - 1);
}
}
int calcLongestFixed(string strMismatch, string pattern ){
int i = strMismatch.length();
int max_index = i - 1;
return calcLongestFixed(strMismatch, pattern, max_index);
}
vector<int> InitVectorNext_my_method(string& pattern) {
vector<int> vecNext;
for (int i = 0; i < pattern.length(); i++) {
string substring = pattern.substr(0, i + 1);
int pos = calcLongestFixed(substring, pattern);
vecNext.push_back(pos);
}
return vecNext;
}
// 作者論文中所描述的方法
vector<int> InitVectorNext_author_method(string &pattern)
{
int N = pattern.length();
vector<int> next;
next.resize(N, 0);
// 初始條件:j=0時,i肯定是不存在的定義爲-1,其他位置值任意。
next[0] = -1;
// 優化前的代碼
vector<int> f;
f.resize(N, -1);
// 初始條件:j=0時,i肯定是不存在的定義爲-1,其他位置值任意。
f[0] = -1;
for (int j = 0; j < N-1;) {
// 先找到pat[1,i-1]=pat[j-i+1,j-1]的情況
int t = f[j];
while (t > -1 && pattern[j] != pattern[t])
t = next[t];
f[j + 1] = t + 1;
j++;
// 判斷pat[i]和pat[j]的情況
if (pattern[j] == pattern[f[j]])
next[j] = next[f[j]];
else
{
next[j] = f[j];
}
}
return next;
}
int search(string& strText, string& pattern, vector<int> &vecNext) {
int i = 0, j = 0;
int N = strText.length();
int M = pattern.length();
for (; i < N && j < M;) {
if (j == -1 || strText[i] == pattern[j]) {
j++; i++;
if (j >= M)
return i - M;
} else {
j = vecNext[j];
}
}
return -1;
}
void testCalcLongestFixed();
int main()
{
testCalcLongestFixed();
/////////////////////////////////////////////////////////////////
cout << "test 1" << endl;
cout << "Expected: -1 0 0 0 -1 0 2" << endl;
string patter_ryf = "ABCDABD";
vector<int> vecNextRYF = InitVectorNext_author_method(patter_ryf);
for (int i = 0; i < vecNextRYF.size(); i++) {
cout << vecNextRYF[i] << " ";
}
cout << endl;
/////////////////////////////////////////////////////////////////
cout << "test 2" << endl;
cout << "Expected: -1 0 0 -1 0 0 -1 4 -1 0" << endl << "Actual: ";
string pattern_paper = "abcabcacab";
vector<int> vecNext_author = InitVectorNext_author_method(pattern_paper);
for (int i = 0; i < vecNext_author.size(); i++) {
cout << vecNext_author[i] << " ";
}
cout << endl;
/////////////////////////////////////////////////////////////////
{
cout << "========My method============" << endl;
string txt1 = "aabracadabra abacadabrabracabracadabrabrabracad";
string pattern1 = "abracadabra";
cout << "===========================" << endl;
vector<int> vecNext = InitVectorNext_my_method(pattern1);
cout << search(txt1, pattern1, vecNext) << endl;
string txt2 = "abacadabrabracabracadabrabrabracad";
//string txt2 = "rrabasdsfsdasdfra";
string pattern2 = "rab";
cout << "===========================" << endl;
vector<int> vecNext2 = InitVectorNext_my_method(pattern2);
cout << search(txt2, pattern2, vecNext2) << endl;
}
{
cout << "========Author's method============" << endl;
string txt1 = "aabracadabra abacadabrabracabracadabrabrabracad";
string pattern1 = "abracadabra";
cout << "===========================" << endl;
vector<int> vecNext = InitVectorNext_author_method(pattern1);
cout << search(txt1, pattern1, vecNext) << endl;
string txt2 = "rrarabasdsfsdasdfra";
string pattern2 = "rab";
cout << "===========================" << endl;
vector<int> vecNext2 = InitVectorNext_author_method(pattern2);
cout << search(txt2, pattern2, vecNext2) << endl;
}
}
void testCalcLongestFixed()
{
string pattern = "aaabc";
string s1 = "aaac"; // aaax處失配
assert(calcLongestFixed(s1, pattern) == 2);
string s2 = "aaabd"; // aaabx處失配
cout << calcLongestFixed(s2, pattern);
assert(calcLongestFixed(s2, pattern) == 0);
}
4.小結
KMP算法主要優化字符查找的效率出發,通過觀察和假設,將問題轉化爲尋找一個最小偏移量的問題,之後進一步將問題轉化爲尋找模式串中前綴和後綴的最大匹配長度。最後通過這個最大匹配長度,反向計算出最小偏移量,得到的問題的解。問題的轉化和化簡,循序漸進,最終得到了這個問題的一個高效解!要不是前前後後翻來覆去的看論文的前幾節的描述,差點這個過程擦肩而過了。
歡迎一起探討相關問題!
5. 引用文獻
1. 論文:FAST PATTERN MATCHING IN STRINGS, DONALD E. KNUTHf, JAMES H. MORRIS
3. 從頭到尾徹底理解KMP(2014年8月22日版)——高閱讀量的,不過我感覺還是沒看明白
4. KMP算法證明