KMP演算法總結(純演算法,為優化,沒有學應用)
KMP 演算法,俗稱“看毛片”演算法,是字串匹配中的很強大的一個演算法,不過,對於初學者來說,要弄懂它確實不易。整個寒假,因為家裡沒有網,為了理解這個演算法,那可是花了九牛二虎之力!不過,現在我基本上對這個演算法理解算是比較透徹了!特寫此文與大家分享分享!
我個人總結了, KMP 演算法之所以難懂,很大一部分原因是很多實現的方法在一些細節的差異。怎麼說呢,舉我寒假學習的例子吧,我是看了一種方法後,似懂非懂,然後去看另外的方法,就全都亂了!體現在幾個方面: next 陣列,有的叫做“失配函式”,其實是一個東西; next 陣列中,有的是以下標為 0 開始的,有的是以 1 開始的;KMP 主演算法中,當發生失配時,取的 next
所以,在闡述我的理解之前,我有必要說明一下,我是用 next 陣列的, next 陣列是以下標 0 開始的!還有,我不會在一些基礎的概念上浪費太多,所以你在看這篇文章時必須要懂得一些基本的概念,例如 “ 樸素字串匹配 ”“ 字首 ” , “ 字尾 ” 等!還有就是,這篇文章的每一個字都是我辛辛苦苦碼出來的,圖也是我自己畫的!如果要轉載,請註明出處!好了,開始吧!
假設在我們的匹配過程中出現了這一種情況:
根據 KMP 演算法,在該失配位會呼叫該位的 next 陣列的值!在這裡有必要來說一下 next 陣列的作用!說的太繁瑣怕你聽不懂,讓我用一句話來說明:
返回失配位之前的最長公共前後綴!
什麼是最長公共前後綴:
好,不管你懂不懂這句話,我下面的文字和圖應該會讓你懂這句話的意思以及作用的!
首先,我們取之前已經匹配的部分(即藍色的那部分!)
我們在上面說到 next 陣列的作用時,說到 “ 最長公共前後綴 ” ,體現到圖中就是這個樣子!
接下來,就是最重要的了!
沒錯,這個就是 next 陣列的作用了 :
返回當前的最長公共前後綴長度,假設為 len 。因為陣列是由 0 開始的,所以 next陣列讓第 len 位與主串匹配就是拿最長字首之後的第 1 位與失配位重新匹配,避免匹配串從頭開始!如下圖所示!
(重新匹配剛才的失配位!)
如果都說成這樣你都不明白,那麼你真的得重新理解什麼是 KMP 演算法了!
接下來最重要的,也是 KMP 演算法的核心所在,就是 next 陣列的求解!不過,在這裡我找到了一個全新的理解方法!如果你懂的上面我寫的的,那麼下面的內容你只需稍微思考一下就行了!
跟剛才一樣,我用一句話來闡述一下 next 陣列的求解方法,其實也就是兩個字:
繼承
a 、當前面字元的前一個字元的對稱程度為 0 的時候,只要將當前字元與子串第一個字元進行比較。這個很好理解啊,前面都是 0 ,說明都不對稱了,如果多加了一個字元,要對稱的話最多是當前的和第一個對稱。比如 agcta 這個裡面 t 的是 0 ,那麼後面的 a 的對稱程度只需要看它是不是等於第一個字元 a 了。
b 、按照這個推理,我們就可以總結一個規律,不僅前面是 0 呀,如果前面一個字元的 next 值是 1 ,那麼我們就把當前字元與子串第二個字元進行比較,因為前面的是 1,說明前面的字元已經和第一個相等了,如果這個又與第二個相等了,說明對稱程度就是 2 了。有兩個字元對稱了。比如上面 agctag ,倒數第二個 a 的 next 是 1 ,說明它和第一個 a 對稱了,接著我們就把最後一個 g 與第二個 g 比較,又相等,自然對稱成都就累加了,就是 2 了。
c 、按照上面的推理,如果一直相等,就一直累加,可以一直推啊,推到這裡應該一點難度都沒有吧,如果你覺得有難度說明我寫的太失敗了。
當然不可能會那麼順利讓我們一直對稱下去,如果遇到下一個不相等了,那麼說明不能繼承前面的對稱性了,這種情況只能說明沒有那麼多對稱了,但是不能說明一點對稱性都沒有,所以遇到這種情況就要重新來考慮,這個也是難點所在。
如果藍色的部分相同,則當前 next 陣列的值為上一個 next 的值加一,如果不相同,就是我們下面要說的!
如果不相同,用一句話來說,就是:
從前面來找子前後綴
1 、如果要存在對稱性,那麼對稱程度肯定比前面這個的對稱程度小,所以要找個更小的對稱,這個不用解釋了吧,如果大那麼就繼承前面的對稱性了。
2 、要找更小的對稱,必然在對稱內部還存在子對稱,而且這個必須緊接著在子對稱之後。
如果看不懂,那麼看一下圖吧!
好了,我已經把該說的儘可能以最淺顯的話和最直接的圖展示出來了,如果還是不懂,那我真的沒有辦法了!
針對KMP演算法我在新增一下我自己人為非常重要的認識
1.KMP的核心在於移位代替回溯,我們通過查找出最長的公共前後綴,從而確定了可以最大效率簡化我們的時間複雜度的移位的最大長度
先附圖在附程式碼(求next陣列的)解釋:
void makeNext(const char P[],int next[])
{
int q,k;//q:模版字串下標;k:最大前後綴長度
int m = strlen(P);//模版字串長度
next[0] = 0;//模版字串的第一個字元的最大前後綴長度為0
for (q = 1,k = 0; q < m; ++q)//for迴圈,從第二個字元開始,依次計算每一個字元對應的next值
{
while(k > 0 && P[q] != P[k])//遞迴的求出P[0]···P[q]的最大的相同的前後綴長度k
k = next[k-1]; //不理解沒關係看下面的分析,這個while迴圈是整段程式碼的精髓所在,確實不好理解
if (P[q] == P[k])//如果相等,那麼最大相同前後綴長度加1
{
k++;
}
next[q] = k;
}
}
下面我們再來講解一下利用next陣列的KMP演算法部分:
先上程式碼:
#include<stdio.h>
#include<string.h>
void makeNext(const char P[],int next[])
{
int q,k;
int m = strlen(P);
next[0] = 0;
for (q = 1,k = 0; q < m; ++q)
{
while(k > 0 && P[q] != P[k])
k = next[k-1];
if (P[q] == P[k])
{
k++;
}
next[q] = k;
}
}
int kmp(const char T[],const char P[],int next[])
{
int n,m;
int i,q;
n = strlen(T);
m = strlen(P);
makeNext(P,next);
for (i = 0,q = 0; i < n; ++i)
{
while(q > 0 && P[q] != T[i]) //這裡我們採取的是移動模式串的策略,可能看不出來,這需要我們畫圖來看
q = next[q-1];
if (P[q] == T[i])
{
q++;
}
if (q == m)
{
printf("Pattern occurs with shift:%d\n",(i-m+1));
}
}
}
int main()
{
int i;
int next[20]={0};
char T[] = "ababxbababcadfdsss";
char P[] = "abcdabd";
printf("%s\n",T);
printf("%s\n",P );
// makeNext(P,next);
kmp(T,P,next);
for (i = 0; i < strlen(P); ++i)
{
printf("%d ",next[i]);
}
printf("\n");
return 0;
}
以上就是我對KMP演算法核心的瞭解
附上自己封裝的KMP演算法的程式碼如下:
#include"iostream"
#include"cstdio"
#include"cstdlib"
#include"cstring"
#define N 100
using namespace std;
template<typename T> class kmp;
template<typename T> istream& operator>>(istream&,kmp<T>&);
template<typename T> ostream& operator<<(ostream&,kmp<T>&);
template<typename T>
class kmp
{
public:
kmp()
{
memset(next,0,sizeof(next));
memset(pattern,0,sizeof(pattern));
memset(mother,0,sizeof(mother));
num=plength=mlength=fpos=0;
}
friend istream& operator>><>(istream&,kmp<T>&);
friend ostream& operator<<<>(ostream&,kmp<T>&);
void getnextone(); //未優化的
void find();
void count();
private:
T pattern[N];
int plength;
T mother[N];
int mlength;
int next[N];
int num; //母串中包含的個數
int fpos;
};
template<typename T>
istream& operator>>(istream& in,kmp<T>& k)
{
cout<<"請輸入母串的長度"<<endl;
cin>>k.mlength;
cout<<"請輸入母串"<<endl;
for(int i=0;i<k.mlength;i++) cin>>k.mother[i];
cout<<"請輸入模式串的長度"<<endl;
cin>>k.plength;
cout<<"請輸入模式串"<<endl;
for(int i=0;i<k.plength;i++) cin>>k.pattern[i];
return in;
}
template<typename T>
ostream& operator<<(ostream& out,kmp<T>& k)
{
cout<<"next陣列的內容如下,以供查錯"<<endl;
for(int i=0;i<k.plength;i++) cout<<k.next[i]<<' ';
cout<<endl;
cout<<"母串中包含的傳的個數是"<<k.num<<endl;
cout<<"第一次出現模式串的位置是"<<k.fpos<<endl;
return out;
}
template<typename T>
void kmp<T>::getnextone()
{
//next[0]=0,因為0號位置沒有字首和字尾
int k=0; //目前最長公共前後綴的長度
int q=1; //q記錄目前掃描的的位置
for(;q<plength;q++) //永遠記住,k代表的是長度,實際上的區間位置是0--k-1適合和額字首
{
while(k>0&&pattern[k]!=pattern[q]) k=next[k-1]; //演算法中描述的部分
if(pattern[k]==pattern[q]) k++; //再次匹配,我們擴充最長公共前後綴
next[q]=k;
}
}
template<typename T>
void kmp<T>::find()
{
int i=0;
int j=0;
getnexttwo();
for(;i<mlength;i++)
{
while(j>0&&pattern[j]!=mother[i]) j=next[j-1];
if(pattern[j]==mother[i]) j++;
if(j==plength)
{
fpos=i-plength+1; //j-fpos+1=k.plength
cout<<"我們找到了匹配的模式串,第一次出現的位置在"<<fpos<<endl;
return ;
}
}
cout<<"母串中不存在匹配的模式串"<<endl;
return ;
}
int main()
{
kmp<int> my;
cin>>my;
my.find();
cout<<my;
return 0;
}