KMP算法是解決字符串查找問題的,給定文本串text和模式串pattern,從文本串text中找出模式串pattern第一次出現的位置。
最基本的字符串匹配算法是暴力求解,時間複雜度爲O(m*n)
KMP算法是一種線性時間複雜度的字符串匹配算法,他是對暴力算法的改進。
記:文本串長度爲N,模式串長度爲M,那麼暴力算法的時間複雜度爲O(M*N),空間複雜度爲O(1),KMP算法的時間複雜度爲O(M+N),空間複雜度爲O(M)
先給出暴力求解算法,比較簡單,如下
// 查找s中首次出現p的位置
int BruteForceSearch(const char* s, const char* p)
{
int i = 0;// 當前匹配到的原始串收尾
int j = 0;// 模式串的匹配位置
int size = (int)strlen(p);
int nLast = (int)strlen(s) - size;
while((i <= nLast) && (j < size))
{
if (s[i + j] == p[j])// 若匹配,則模式串匹配位置後移
{
j++;
}
else // 不匹配,則對比下一個位置,模式串回溯到位首
{
i++;
j = 0;
}
}
if (j >= size)
{
return i;
}
return -1;
}
KMP分析
先看一個圖,改圖表示了當匹配到黃色和綠色失配的情況下KMP的處理過程我們可以不用將j不用移到串首,而是移動到串的某個位置
我們爲什麼可以這樣做呢,可以看下圖,下圖是上面移動的過程的放大圖
如果我們要這樣做,必定有個前提,A和B肯定是相同的。那麼我們只需要找到d以前的相等的最長前綴串和最長後綴串,下面看一下怎麼得到這個前綴串
有如下例子
如:j=5時,考察字符串“abaab”的最大相等k前綴和k後綴如下圖
顯然最大且相等的是ab,所以如果匹配到c的位置發現不匹配,這個時候i就不需要動了,j就可以回溯到next[j]也就是2,然後模式串從2,文本串從i繼續匹配,最好的情況恰好是next[j]=0的時候,因爲他們沒有相同的前綴和後綴,所以我們可以知道在i和j中間怎麼移動都不會有匹配了,所以直接將向後移動j位,j回溯到0就可以了,這樣滑動是最快的,相反next[j]越大滑動越慢,注意理解i的移動和暴力算法下的區別,解釋完畢。
那麼現在比較重要的應該是怎麼去獲得這個next數組了,P爲模式串,如下圖
對於模式串的位置j,有next[j]=k,即:P0P1...Pk-1=Pj-kPj-k+1...Pj-2Pj-1則,對於模式串的位置j+1,考察Pj:
若P[k]==P[j]
則next[j+1]=next[j]+1
若P[k]!=P[j],那麼我們知道A和B是相等的,我們需要在B中找所以
記h=next[k](next[k]肯定是已經求出來了),如果P[h]==P[j],則next[j+1]=h+1,爲什麼呢,因爲A=B,1=3,所以1=2,所以可以的到上面結論,否則重複此過程。
分析完畢,給出求next的代碼
void GetNext(char* p, int next[]) { int nLen = (int)strlen(p); next[0] = -1; int k = -1; int j = 0; while (j < nLen - 1) { // 此刻,k即next[j-1],且p[k]表示前綴,p[j]表示後綴 // 住:k==-1表示未找到k前綴與k後綴相等,首次分析可先忽略 if (k == -1 || p[j] == p[k]) { ++j; ++k; next[j] = k; } else // p[j]與p[k]失配,則繼續遞歸計算前綴p[next[k]] { k = next[k]; } } }
接下來給出KMP的代碼char* g_s = "dsaqeabaabcabaffd"; char* g_pattern = "abaabcaba"; int KMP() { int ans = -1; int i = 0; int j = 0; int pattern_len = strlen(g_pattern); int g_next[100] = {0}; GetNext(g_pattern, g_next); while (i < strlen(g_s)) { if (j == -1 || g_s[i] == g_pattern[j]) { ++i; ++j; } else { j = g_next[j]; } if (j == pattern_len) { ans = i - pattern_len; break; } } return ans; }
分析BF與KMP的區別
- 假設當前文本串text匹配到i位置,模式串pattern匹配到j位置。
- BF算法中,如果當前字符串匹配成功,即text[i+j]==pattern[j],令j++,繼續匹配下一個字符。若適配,即text[i+j]!=pattern[j],令i++,j=0,即匹配失敗是,模式串pattern相當於文本串向右移動了一位。
- KMP算法中,若當前字符串匹配成,即text[i+j]==pattern[j],令j++,繼續匹配下一個字符。若失配,即text[i+j]!=pattern[j],令j=next[j](next[j]<=j-1),即模式串pattern相對於文本串text向右移動至少一位(實際移動位數爲:j-next[j]>=1)
到這裏感覺可以鬆口氣了,但是告訴你,還沒完呢,不要氣餒,繼續前進吧
進一步分析next
- 文本串匹配到i,模式串匹配到j,此刻,若text[i] != pattern[j],即失配的情況:
- 若next[j]=k,說明模式串應該從j滑動到k位置
- 若此時滿足pattern[j]==pattern[k],因爲next[i]!=pattern[j],所以,text[i]!=pattern[k]
- 即i和k沒有匹配,應該繼續滑動next[k]。
- 換句話說:在原始的next數組中,若next[j]=k並且pattern[j]==pattern[k],next[j]可以直接等於next[k]。
以最後一個a爲例,它的原始next爲2,但是P[2]是跟他相等的,所以這時候再去匹配P[2]肯定也不行,那麼直接就把原來的next更新爲2下面的next,應該很好理解的,因爲值越小效率月快,所以變種後的比較優,那麼下面給出變種後的next代碼
void GetNext(char* p, int next[])
{
int nLen = (int)strlen(p);
next[0] = -1;
int k = -1;
int j = 0;
while (j < nLen - 1)
{
// 此刻,k即next[j-1],且p[k]表示前綴,p[j]表示後綴
// 住:k==-1表示未找到k前綴與k後綴相等,首次分析可先忽略
if (k == -1 || p[j] == p[k])
{
++j;
++k;
if (p[j] == p[k])
{
next[j] = next[k];
}
else
{
next[j] = k;
}
}
else // p[j]與p[k]失配,則繼續遞歸計算前綴p[next[k]]
{
k = next[k];
}
}
}
理解KMP的時間複雜度
我們考察模式串的“串頭”和主串的對應位置(也就是暴力算法中的i);
不匹配:穿透後移,保證儘快結束算法
匹配:穿透保持不動(僅僅是i++、j++,但穿透和主串對應位置沒變,但一旦發現不匹配,會跳過一賠過的字符(next[j]))。
最壞的情況,當穿透魚尾N-M的位置,算法結束
因此,匹配的時間複雜度爲O(N),算上next的O(M),整體時間複雜度爲O(M+N)。
下面給出一個KMP算法的應用PowerString週期串
給定一個長度爲n的字符串S,如果存在一個字符轉T,重複若干次T能夠得到S,那麼,S叫作週期串,T叫做S的一個週期
如:字符串abababab是週期串,abab、ab都是它的週期,其中,ab是它的最小週期。
設計一個算法,計算S的最小週期。如果S不存在週期,返回空串。
對於下面三個條帶圖我們從第一個開始分析,他是最長前綴和最長後綴,從圖中的得到的信息是中間部分相等,爲什麼他們相等呢,其實這個問題我思考了好長時間,最後恍然大悟,他們是重合的,所以就相等了,呵呵,那我們依次向後同時取t長度,則得出上下對應相等,又因爲前提是len-k可以被t整除,所以得出一共有t個串,所以t是他的週期了。
最後一個題目不是特別理解,就理解到這裏吧,估計應付面試應該沒問題了。