1. 程式人生 > >KMP的個人向總結(面向自己)--by wxj

KMP的個人向總結(面向自己)--by wxj

之前學過KMP演算法,但是當時學的時候就是比較模糊,對於它的認知也處在會用,會寫的層次,但是對於它的內部的實現原理,仍是似懂非懂的狀態,現在老師講到字串匹配演算法的時候,我就重新學了一遍KMP,感覺之前有所疑惑的地方清晰了許多,趁現在對KMP仍有比較清晰的認知(個人覺得),趕緊記錄下來,便於以後溫習;

KMP,字串的匹配演算法,複雜度O(n+m),母串n+子串m,也就是說遍歷一遍就可得出答案,相對傳統的暴力匹配O(n*m)快了不是一星半點,當然也繞的不是一星半點;

KMP演算法就是跳過重複的或著必定不會是答案的那段字串,來節省不必要的時間;

例如:(存在返回首個下標,不存在返回-1)母串中查詢子串;

母串:abdabcabda

子串:abcab;

這麼簡單的我們當然可以肉眼直接看了,但是計算機是不會直接看的,正常情況下,一個一個比較(暴力m*n)比較簡單,也容易實現,就不寫了,沒什麼意思,下面我們用KMP的方法查詢母串:

首先我們要處理出子串的回溯陣列回溯陣列是KMP的靈魂,它的作用就是在子串失配時直接跳轉到失配字元的上一個配對字元的位置;

子串的處理結果如下:(下標從0開始)

 a b c a b

-1 0 0 0 1

這些數字的含義就是該位置的字元可回溯的位置;

看不懂沒關係,我們可以先這樣寫回溯陣列:(下標從1開始)

 a b c a b

 0 0 0 1 2

這個意思很容易看懂吧,就是第i個字元是從頭開始的第幾位(和前面的那個位置的字元時相等的)

很明顯,abc是第一組,ab可以在前面找到abc中的ab,因此對應的是1,2

將這個陣列向右移動一位,前面補-1,即可得出回溯的陣列;

得到了回溯陣列,接下來就可以根據回溯陣列進行快速匹配了;

首先,比較:(以後我預設以下標從0開始)

abdabcabda

abcab

-1 0 0 0 1

很顯然,在第2處不一樣,因為c的前面沒有重複的元素(回溯到0),因此直接就可以跳到0的位置:

abdabcabda

   
abcab

這樣就相當於跳過了母串1位置元素的匹配了(因為這個位置是必定不可能匹配到的,可證明但我不證明);

然後再次比較:再往後移動

abdabcabda

      abcab

然後發現,匹配成功!

當然這個只是簡單的例子,原理就是這個了,下面開始介紹如何求解回溯陣列;

KMP的靈魂是回溯陣列的應用(個人感覺)

程式碼鎮樓:(下面解釋)

//            p子串  lp子串長度  nxt回溯陣列儲存的陣列
void get_nxt(char *p,int lp,int nxt[])
{//對於一個子串 ,nxt陣列記錄的是第i位置的字元他之前重複出現的字元的位置(如果在首位置,則全面的字元為-1) 
	nxt[0]=-1;//第0個元素沒有前面相同的字元,初始化-1 
	int k=-1,j=0;//k(前面元素的位置), 
	while(j<lp)
	{
		if(k==-1||p[j]==p[k])//該字元在首位置||字元相等(可回溯到前面)
		{
			++k;
			++j;
			nxt[j]=k;//該位置能夠回溯的位置
		}
		else
			k=nxt[k];//如果字元不等,繼續向前回溯
	}
}

附帶一個樣例:

i:ababdababc

j:ababc

-1 0 0 1 2

第一次位移:

i:ababdababc

j:    ababc

j=2;p[j]='a';比較a和d,

i:ababdababc

j:        ababc

然後繼續j=0比較a個d,

i:ababdababc

j:          ababc

之後j=-1,向後移動。

反正我看了這個樣例之後很清楚

KMP比較的程式碼比較簡單,就是按照上面的跑一邊即可:

//          s母串  p子串  ls母串長度 lp子串長度  nxt陣列儲存回溯位置
void KMP(char *s,char *p,int ls,int lp,int nxt[])
{
	int ans=-1,i=0,j=0;
	while(i<ls)
	{
		//cout<<i<<" "<<j<<endl;
		if(j==-1||s[i]==p[j])//配對繼續走
		{
			++i;
			++j;
		}
		else//失配回溯
			j=nxt[j];
		if(j==lp)//檢視是否匹配完成
		{
			cout<<i-lp<<endl;//返回母串的下標
			return ;
		}
	}
	cout<<"NO FIND!"<<endl;//母串中沒有子串
}

這樣就比較清楚了,寫完之後覺得比較清晰了。還是太菜啊!一個KMP拖到現在......

最後的是我的測試程式碼:

//#pragma comment(linker, "/STACK:1024000000,1024000000") 

#include<stdio.h>
#include<string.h>  
#include<math.h>  
  
//#include<map>   
//#include<set>
#include<deque>  
#include<queue>  
#include<stack>  
#include<bitset> 
#include<string>  
#include<fstream>
#include<iostream>  
#include<algorithm>  
using namespace std;  

#define ll long long  
//#define max(a,b) (a)>(b)?(a):(b)
//#define min(a,b) (a)<(b)?(a):(b) 
#define clean(a,b) memset(a,b,sizeof(a))// 水印 
//std::ios::sync_with_stdio(false);
const int MAXN=1e5+10;
const int INF=0x3f3f3f3f;
const ll mod=1e9+7;

void get_nxt(char *p,int lp,int nxt[])
{//對於一個子串 ,nxt陣列記錄的是第i位置的字元他之前重複出現的字元的位置(如果在首位置,則全面的字元為-1) 
	nxt[0]=-1;//第0個元素沒有前面相同的字元,初始化-1 
	int k=-1,j=0;//k(前面元素的位置), 
	while(j<lp)
	{
		if(k==-1||p[j]==p[k])//如果第一次出現 
		{
			++k;
			++j;
			nxt[j]=k;
		}
		else
			k=nxt[k];
	}
}

void KMP(char *s,char *p,int ls,int lp,int nxt[])
{
	int ans=-1,i=0,j=0;
	while(i<ls)
	{
		//cout<<i<<" "<<j<<endl;
		if(j==-1||s[i]==p[j])
		{
			++i;
			++j;
		}
		else
			j=nxt[j];
		if(j==lp)
		{
			cout<<i-lp<<endl;
			return ;
		}
	}
	cout<<"NO FIND!"<<endl;
}
/*
3
abdbababcadd
abcab
abdbbcabcbab
abcab

*/
int main()
{
	int T;
	cin>>T;
	while(T--)
	{
		char s[MAXN],p[MAXN];
		//s母串,p子串 
		int nxt[MAXN];
		//子串中記錄前驅字元的位置陣列 
		clean(nxt,0);//初始化 
		cin>>s>>p;
		int lp=strlen(p),ls=strlen(s);
		get_nxt(p,lp,nxt);//獲取nxt陣列 
//		for(int i=0;i<=lp;++i)
//			cout<<nxt[i]<<" ";
//		cout<<endl; 
		KMP(s,p,ls,lp,nxt);
	}
}