1. 程式人生 > >KMP演算法總結(純演算法,為優化,沒有學應用)

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;
}