1. 程式人生 > 其它 >理解資料結構教材中--KMP演算法

理解資料結構教材中--KMP演算法

技術標籤:演算法c語言資料結構

KMP演算法詳解

1.一篇博文

KMP演算法是一種字串匹配演算法,第一次接觸KMP演算法是在資料結構的教材中。為了弄懂這個演算法,我在網上查閱了很多資料,很多博文都說其內容很好理解,但本人看完總是似懂非懂。
這個演算法在掌握後會覺得非常簡單,可其描述總是有些令人難以理解,這裡推薦一篇2013年的文章,相信你看完後一定能理解KMP演算法
KMP演算法博文
不過有一個問題是,此理解方法似與《資料結構與演算法(第四版 廖明巨集)》中的P63頁對於KMP演算法的解釋有所出入。

2.書中解釋

對於書中的解釋,其重點為一個公式:

N e x t [ j ] = { 0 , j = 1 M a x { k ∣ 1 < k < j } , S u b s t r ( T , 1 , k − 1 ) = S u b s t r ( T , j − k + 1 , k − 1 ) 1 , 其他情況 Next[j]= \begin{cases} 0,&\text{$j=1$}\\ Max\{k|1<k<j\},&\text{$Substr(T,1,k-1)=Substr(T,j-k+1,k-1)$}\\ 1,&\text{其他情況} \end{cases} Next[j]=
0,Max{k1<k<j},1,j=1Substr(T,1,k1)=Substr(T,jk+1,k1)其他情況

注:

  • 書中的字串第一個字元的索引位是1。
  • 函式 S u b s t r ( T , n , m ) Substr(T,n,m) Substr(T,n,m)的含義是:對於字串T,從第n位開始,擷取m個字元。也就是擷取一段字串。
  • 舉例: t [ 20 ] = " 8 h e l l o t o m " t[20]="8hellotom" t[20]="8hellotom" S u b s t r ( t , 2 , 4 ) = " e l l o " Substr(t,2,4)="ello"
    Substr(t,2,4)="ello"
    . t [ 0 ] t[0] t[0]存放字串長度,因此第一個字元的索引位是1。

模式串 t t t 的每一個 t j t_j tj 都對應一個 k k k 值,此 k k k 值僅依賴於模式串 t t t 本身字元序列的構成,而與主串 s s s 無關。用 n e x t [ j ] next[j] next[j] 表示 t j t_j tj 對應的 k k k 值。

3.對比書與博文

我們先以簡單的語言來概括書與博文的要點。

  • 書中,重點為公式:
    S u b s t r ( T , 1 , k − 1 ) = S u b s t r ( T , j − k + 1 , k − 1 ) Substr(T,1,k-1)=Substr(T,j-k+1,k-1) Substr(T,1,k1)=Substr(T,jk+1,k1)
  • 博文中,我們不妨用 t a b l e [ j ] table[j] table[j]來表示部分匹配表,這個表中的部分匹配值就是字首字尾最長共有元素的長度。

這裡我們用一個例子來說明:

模式串 t = " a b a a b c a c " t="abaabcac" t="abaabcac" 或可寫成 t = " 8 a b a a b c a c " t="8abaabcac" t="8abaabcac",求其 n e x t next next t a b l e table table

書中:

j12345678
n e x t [ j ] ( 也 即 k ) next[j](也即k) next[j](k)01122312

博文:

j12345678
t a b l e [ j ] table[j] table[j]00112010

初看,我們會感覺 n e x t next next t a b l e table table毫無關聯,於是我們就會思考為什麼是這個結果。
其實很好理解,在博文中提到了所謂的部分匹配表的用法:

模式串移動位數 = 已匹配的字元數 - 最後一個正確匹配位對應的 t a b l e table table

而這個 t a b l e table table表的含義是:字首字尾最長共有元素的長度。

再看書中公式: S u b s t r ( T , 1 , k − 1 ) = S u b s t r ( T , j − k + 1 , k − 1 ) Substr(T,1,k-1)=Substr(T,j-k+1,k-1) Substr(T,1,k1)=Substr(T,jk+1,k1)
相信大家都感覺到了,這個公式中的 k-1也隱含表達:字首字尾共有元素的長度,再加上一個 M a x { k ∣ 1 < k < j } Max\{k|1<k<j\} Max{k1<k<j},即表示最長共有元素的長度。

  • 其中字首為: t 1 t_1 t1~ t k − 1 t_{k-1} tk1字尾為: t j − k + 1 t_{j-k+1} tjk+1 ~ t j − 1 t_{j-1} tj1
  • 並且其最長共有元素的長度為: k − 1 k-1 k1,而這同時也是 t a b l e table table表的含義。即 k − 1 = t a b l e [ j ] k-1=table[j] k1=table[j],即 n e x t [ j ] − 1 = t a b l e [ j ] next[j]-1=table[j] next[j]1=table[j]

然而我們對上述兩表根據 n e x t [ j ] − 1 = t a b l e [ j ] next[j]-1=table[j] next[j]1=table[j] 關係對照發現,笑死,根本對不上。

其實只是因為我們忽略了一個小問題:
對於公式:字首為: t 1 t_1 t1~ t k − 1 t_{k-1} tk1字尾為: t j − k + 1 t_{j-k+1} tjk+1 ~ t j − 1 t_{j-1} tj1
我們發現字尾的結尾是 t j − 1 t_{j-1} tj1 而不是 t j t_j tj ,這就是問題所在。相信看到這裡童鞋們一定恍然大悟了,沒錯,我們得錯開一位
對於 n e x t next next表,它的第 j j j 位的公式對應的字尾是 t j − 1 t_{j-1} tj1 ,也就是對應 t a b l e table table表的第 j − 1 j-1 j1 位。即有對應關係: n e x t [ j ] − 1 = t a b l e [ j − 1 ] next[j]-1=table[j-1] next[j]1=table[j1]

如此看來:
t a b l e [ 1 ] = 0 = n e x t [ 2 ] − 1 = 1 − 1 table[1]=0=next[2]-1=1-1 table[1]=0=next[2]1=11
t a b l e [ 2 ] = 0 = n e x t [ 3 ] − 1 = 1 − 1 table[2]=0=next[3]-1=1-1 table[2]=0=next[3]1=11
t a b l e [ 3 ] = 1 = n e x t [ 4 ] − 1 = 2 − 1 table[3]=1=next[4]-1=2-1 table[3]=1=next[4]1=21
t a b l e [ 4 ] = 1 = n e x t [ 5 ] − 1 = 2 − 1 table[4]=1=next[5]-1=2-1 table[4]=1=next[5]1=21
t a b l e [ 5 ] = 2 = n e x t [ 6 ] − 1 = 3 − 1 table[5]=2=next[6]-1=3-1 table[5]=2=next[6]1=31
t a b l e [ 6 ] = 0 = n e x t [ 7 ] − 1 = 1 − 1 table[6]=0=next[7]-1=1-1 table[6]=0=next[7]1=11
t a b l e [ 7 ] = 1 = n e x t [ 8 ] − 1 = 2 − 1 table[7]=1=next[8]-1=2-1 table[7]=1=next[8]1=21
table[8]=0=next[9]-1=???-1
好的,現在都對應上了。。。除了 n e x t [ 9 ] next[9] next[9]不存在,也就是 t a b l e [ 8 ] table[8] table[8]對應不上,這又是為什麼呢?

我們要理解 t a b l e table table表的用法,當 t t t 有一位和主串 s s s 不同,而之前都相同,此時就要用到 t a b l e table table表。我們不妨設不同的那位是 t k t_k tk 。現在計算模式串需要移動的位數:

模式串需要移動的位數 = 已匹配位數 - 最後一個正確匹配位對應的 t a b l e table table

即: 移 動 位 數 = ( k − 1 ) − t a b l e [ k − 1 ] 移動位數 = (k-1)-table[k-1] =(k1)table[k1]

好,現在迴歸之前的問題: t a b l e [ 8 ] table[8] table[8]對應不上。
我們看看什麼情況下才會用到 t a b l e [ 8 ] table[8] table[8] k = 9 k=9 k=9時對吧,可是模式串一共只有8位,第9位根本不用考慮!

明白了吧,其實 t a b l e [ 8 ] table[8] table[8] 根本就用不到,我們不求它都可以。

唉,好像又出現了個問題,就是當模式串 t t t 的首位就與主串 s s s 不同,這時我們用公式:

模式串需要移動的位數 = 已匹配位數 - 最後一個正確匹配位對應的 t a b l e table table

已匹配位數為0,最後一個正確匹配位對應的 t a b l e table table 值沒有定義。這就是問題所在。
解決方法很簡單:

  1. 我們給 t a b l e [ 0 ] table[0] table[0] 定義為 -1。並且其仍滿足 n e x t next next t a b l e table table 的對應關係: t a b l e [ 0 ] = − 1 = n e x t [ 1 ] − 1 = 0 − 1 table[0]=-1=next[1]-1=0-1 table[0]=1=next[1]1=01
    這樣需要移動的位數 = 0 - (-1) = 1位 。。。 over
  2. 在程式碼中遇到這種情況,僅需用if判斷出來,然後手動令其移動1位即可。。。over

好了,至此就沒啥問題了。總結來看,KMP演算法是利用了模式串本身存在的重複性進行優化的,這種重複性即字首字尾那裡。似乎很好理解。

4. 程式碼

既然博文與教材並不衝突,那麼下面給出利用 t a b l e table table 表實現KMP演算法的c語言程式碼。其優點就在於 t a b l e table table 表的計算很好理解,即字首字尾最長共有元素的長度。

  • 程式碼中的索引用 t 0 t_0 t0代表第一個字元
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void my_gettable(char *t,int table[]);
int my_kmp(char *s,char *t,int table[]);

int main(){
	char s[20]="abaabcaabaabcaca",t[20]="abaabcac";
	int table[20];
	int i,m;
	my_gettable(t,table);
	for(i=0;i<strlen(t);i++)
		printf("table[%d]=%d\n",i,table[i]);
	m=my_kmp(s,t,table);
	if(m==-1)
		printf("沒找到!");
	else
		printf("首次出現索引:%d",m);
	
	return 0;
}

void my_gettable(char *t,int table[]){
	int i,j=0,k,m;
	for(i=0;i<strlen(t);i++){
		table[i]=i;
		k=j+1;
		while(k<=i){
			for(m=k;m<=i;m++){
				if(t[j]!=t[m]){
					table[i]--;
					break;	
				}
				else
					j++;
			}
			j=0;
			k++;
			if(m==i+1)
				break;
		}
	}
}

int my_kmp(char *s,char *t,int table[]){
	int i=0,j=0;//i為s的指標,j為t的指標 
	while(i<strlen(s) && j<strlen(t)){
		if(s[i]==t[j]){
			i++;
			j++;
		}
		else if(j==0)//這就是上面提到的用if判斷出來,手動加1的地方
			i++;
		else 
			j=table[j-1]; //這裡很好理解,不必考慮什麼遞迴思想什麼的。
			//先按照博文思路,計算出模式串需要移動的位數,已匹配位數:j ,所以移動位數m=j-table[j-1].
			//接下來可以自己畫圖理解:把串後移m位,相當於把串t的指標前移m位。即j=j-m=j-(j-table[j-1])=table[j-1]
	}
	if(j<strlen(t))
		return -1;
	else
		return i-strlen(t);
}

上述程式碼執行結果