理解資料結構教材中--KMP演算法
KMP演算法詳解
1.一篇博文
KMP演算法是一種字串匹配演算法,第一次接觸KMP演算法是在資料結構的教材中。為了弄懂這個演算法,我在網上查閱了很多資料,很多博文都說其內容很好理解,但本人看完總是似懂非懂。
這個演算法在掌握後會覺得非常簡單,可其描述總是有些令人難以理解,這裡推薦一篇2013年的文章,相信你看完後一定能理解KMP演算法。
KMP演算法博文
不過有一個問題是,此理解方法似與《資料結構與演算法(第四版 廖明巨集)》中的P63頁對於KMP演算法的解釋有所出入。
2.書中解釋
對於書中的解釋,其重點為一個公式:
注:
- 書中的字串第一個字元的索引位是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"
模式串 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,k−1)=Substr(T,j−k+1,k−1) - 博文中,我們不妨用 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
書中:
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
n e x t [ j ] ( 也 即 k ) next[j](也即k) next[j](也即k) | 0 | 1 | 1 | 2 | 2 | 3 | 1 | 2 |
博文:
j | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
t a b l e [ j ] table[j] table[j] | 0 | 0 | 1 | 1 | 2 | 0 | 1 | 0 |
初看,我們會感覺
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,k−1)=Substr(T,j−k+1,k−1)
相信大家都感覺到了,這個公式中的 k-1也隱含表達:字首和字尾的共有元素的長度,再加上一個
M
a
x
{
k
∣
1
<
k
<
j
}
Max\{k|1<k<j\}
Max{k∣1<k<j},即表示最長的共有元素的長度。
- 其中字首為: t 1 t_1 t1~ t k − 1 t_{k-1} tk−1,字尾為: t j − k + 1 t_{j-k+1} tj−k+1 ~ t j − 1 t_{j-1} tj−1
- 並且其最長的共有元素的長度為: k − 1 k-1 k−1,而這同時也是 t a b l e table table表的含義。即 k − 1 = t a b l e [ j ] k-1=table[j] k−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]。
然而我們對上述兩表根據 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}
tk−1,字尾為:
t
j
−
k
+
1
t_{j-k+1}
tj−k+1 ~
t
j
−
1
t_{j-1}
tj−1
我們發現字尾的結尾是
t
j
−
1
t_{j-1}
tj−1 而不是
t
j
t_j
tj ,這就是問題所在。相信看到這裡童鞋們一定恍然大悟了,沒錯,我們得錯開一位。
對於
n
e
x
t
next
next表,它的第
j
j
j 位的公式對應的字尾是
t
j
−
1
t_{j-1}
tj−1 ,也就是對應
t
a
b
l
e
table
table表的第
j
−
1
j-1
j−1 位。即有對應關係:
n
e
x
t
[
j
]
−
1
=
t
a
b
l
e
[
j
−
1
]
next[j]-1=table[j-1]
next[j]−1=table[j−1]
如此看來:
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=1−1
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=1−1
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=2−1
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=2−1
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=3−1
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=1−1
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=2−1
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] 移動位數=(k−1)−table[k−1]。
好,現在迴歸之前的問題:
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 值沒有定義。這就是問題所在。
解決方法很簡單:
- 我們給
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=0−1。
這樣需要移動的位數 = 0 - (-1) = 1位 。。。 over - 在程式碼中遇到這種情況,僅需用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);
}