KMP演算法詳細講解
文章篇幅有點長,但是最後一張圖真的很棒,希望讀者堅持慢慢看到最後,必有收穫。
字串單模式問題描述:
給定文字串text,和模式串pattern,在文字串text中找到模式串pattern第一次出現的位置。
一、最基本的字串單模式匹配演算法:暴力求解(Brute Force):時間複雜度O(m*n)
設文字串text = "ababcabcacbab",模式串為patten = "abcac" 其匹配過程如下圖所示。
黑色線條代表匹配位置,紅色斜槓代表失配位置。
可以看到每次失配之後,模式串都向右移動一位在去從第一個字元開始於文字串匹配。並且
在已經知道很多字元都配不上的情況下,還要這樣移動著去配,是非常浪費時間的。
BF演算法:
- int Brute_ForceSearch(string t,string p)
- {
- int i=0,j=0;
- int len = t.length(),len_p = p.length();
- while((i <len)&&(j<len_p)){
- if(t[i+j]==p[j])///若匹配則模式串後移
- j++;
- else{///不匹配,則比較下一個位置,模式串回到0位
- i++;
-
j = 0;
- }
- }
- if(j >= len_p)
- return i;
- return -1;
- }
在BF中,假如從文字串的第i個字元來開始於模式串匹配。當匹配到模式串的第j位發現失配
即text[i+j] != patten[j]的時候,我們又從文字串的第i+1個位置來重新開始匹配。儘管我們已經
知道了好多字元其實根本就匹配不上,我們還是進行了這個過程,這個時候回溯的過程會非常
耗費我們的時間。而KMP演算法的實質就是,當遇到text[i+j] != patten[j]的時候,但是我們知道
模式串中的 0~j-1 位置上的字元已經於i ~ i+j-1位置上的字元是完全匹配的。這樣我們可以在0~j-1
中找到一個字首A和字尾B相等並且最長的那個串,然後將A移動到B的位置再開始重新匹配即可。
這樣就減少了一些不必要的匹配。時間複雜度O(n)
Next陣列的求法:
普及兩個概念:
字首和字尾:例如一個字串:abcd
它的字首有 它的字尾有
a d
ab cd
abc bcd
我們這裡所說的字首、字尾不包括字串自身。
求next陣列的時候,對於模式串的位置j,考察patten[j-1].查詢字串patten[j-1]的最大相等的字首和字尾。
假設最大相等字首和字尾長度為k,則有k使得 p[0]p[1]p[2]......p[k-2]p[k-1] = p[j-k]p[j-k+1]......p[j-2]p[j-1]。
例如模式串Patten = "abaabcac"。其next陣列如圖所示:
我們可以看圖中第一個c字元的下標是5,其next陣列的值是2.也就是說,模式串裡面當配到c這個
字元失配的時候,再文字串中,abaab都是已經配好的,我們發現patten前面出現過ab,所以我們從
之前的ab串的後一個字元繼續匹配就行了。如下圖所示:
從圖中我們可以看出藍色位置位置失配,藍色位置前面的字串中,最長公共字首字尾是ab,則我們可以直接
把模式串patten向右滑,讓黃色位置格子於文字串失配的地方對其。則藍色格子失配即模式串第6個字元失配,
就從模式串的第三個字元開始配,第三個字元的下標為2,則next[5] = 2;字串滑過去後如下圖所示。
這樣我們一下滑過去,就跳過了文字串中的text[1],text[2]。避免了這些不必要的匹配。
現在我們開始來講求next陣列。巨集觀上我們按下圖的步驟求next。
我們用index來表示next陣列的下標:
當index = 2時,求ab的最大相等字首串,字尾串。
當index = 3時,求aba的最大相等字首串,字尾串。
當index = 4時,對abaa求最大相等字首串、字尾串。
當j=5時,求abaab的最大相等字首串,字尾串。
。。。。。。。。。。。。。。。。。。不在往後求了,就是按照這樣的方法一直求下去。
看一下程式碼實現:
- /*
- 付完整程式碼
- */
- #include <iostream>
- #include <string.h>
- #include <stdio.h>
- using namespace std;
- const int maxn = 1000010;
- char text[maxn];
- char patten[maxn];
- int next[maxn];
- void GetNext()
- {
- int len_p = strlen(patten);
- next[0] = -1;
- int k = -1,j = 0 ;
- while(j<len_p)
- {
- if(k == -1||patten[j] == patten[k])
- {
- ++k;++j;
- next[j] = k;
- }
- else
- k = next[k];
- }
- }
- int KMP()
- {
- int ans=-1,i = 0,j = 0;
- int len_p = strlen(patten),n=strlen(text);
- while(i<n)
- {
- if(j==-1||text[i] == patten[j])
- {
- ++i;++j;
- }
- else
- j = next[j];
- if(j == len_p){
- return i-len_p;
- break;
- }
- }
- return ans;
- }
- int main()
- {
- int t;
- scanf("%d",&t);
- while(t--)
- {
- scanf("%s%s",patten,text);
- GetNext();
- for(int i = 0; i <strlen(patten); i++)
- printf("%d ",next[i]);
- printf("\n");
- printf("%d\n",KMP());
- }
- return 0;
- }
當然大家都直到程式碼該怎麼寫,也知道上面找前後綴的過程,但是肯定看程式碼的時候還是會覺得一臉懵。
想當初我也是這樣的,理解前後綴,但是就是看不懂程式碼,所以為了幫助理解程式碼我又搞了下面這個圖。
這時我這篇部落格裡面最棒的一個圖片了,個人認為: