KMP算法分析

为什么会产生KMP算法?

 

问题:给定一下字符串S,找出S中匹配W的起始位置?

 

解决这个问题,一般来讲,可以从S的第0个字符开始,看与W的第0个字符是否相等,如果相等再比较第1个字符,如果相等继续比较,如果不相等,再回到S的第1个字符,与W的第0个字符相比较。

 

过程如下图所示:

S:ABCFABCDABFABCDABCDABDE

W:ABCDABD



1. 比较发现W的第3个字符与S的第3个字符不相等,中止比较

2. 从S的第1 个字符开始S的第0个字符开始比较,不相等,中止比较

3. 从S的第2个字符开始比较,不相等,中止比较

4. 从S的第3个字符开始比较……

 

观察图中现象,第1次比较时,S和W的前三个字符是相等的,那么第二次比较等同于W的第0个字符与W的第1个字符比较,第三次比较等同于W的第0个字符与W的第2个字符比较。

 

再观察第5个,第5步的W的最后一个字符与S的第10个字符不相等,中止比较。但从比较的过程中,可以确定,W的0-5个字符与S的4-9个字符是相等的。W[0…5] == S[4…9]

 

第5步和第12步,都是在最后一个字符发现不相等,进而中止比较,然后后面的三步的比较过程是惊人的相似!

 

有没有办法跳过重复的比较过程? 如第12步,可以直接进入到第16步,第4-5的AB与0-1的AB相等,是不是可以直接从W的第3个字符开始比较

 

上面的描述未免有点一相情愿,缺乏理论根基,Knuth, Morris, Pratt 三个大神已经给出了处理算法:寻找最长首尾匹配位置,该算法对W串进行预处理

 

算法描述

依据wikipedia中的描述,重新画出KMP算法的处理流程:

其中

S表示待查找的字符串,

W表示模式字符串,

m表示匹配的模式字符串在S中的起始位置,

i表示比较时W字符串的位置指针

 

第一步,m = 0, S[3]!=W[3]。 W[0]与W[1]、W[2]均不相等, 所以W[0]与S[1]、S[2]均不相等。调整 m = 3, i = 0;

第二步,S[3] != W[0],从S的下一个位置开始比较,m = 4, i = 0

第三步,S[4, 9] = W[0,5], S[10] != W[6]。W[1-3]中均与W[0]不相等,对应的S[5-7]中不会存在与W[0]匹配的字符,而W[0, 1]与W[4,5]相等,下一步比较S[10]与W[2], 此时m = 10 -2 = 8, i = 2;

第四步,m = 8, i = 2,由于W[0]与S[8]、W[1]与S[9]相等,第四步的比较直接从i=2开始,S[10] != W[2],W[0] !=W[1],  W[0] !=S[9],  调整 m = 10, i = 0;

第五步,W[0] !=S[10], 从S的下一个字符开始比较,调整 m = 11, i=0

第六步,同第三步,W[6] != S[11 + 6], 而S[15,16] == W[4,5]==W[0,1], 调整m = 15,i = 2;

第七步,找到匹配的W,在S中的起始位置为15。

 

假设存在局部匹配表T,用于指示当前字符比较失败后下一次比较从哪开始。T的每一项构造满足如下关系: 从S[m]开始比较,当S[m+i]不等于W[i]时,比较从S[m+ i – T[i]]字符串开始。这里有两层暗示:

1.    T[0] = -1,即W[0]不匹配的时候,S不需要回溯,需要从下一个字符开始比较。

2.    尽管下一个可能匹配开始的节点起点在 m + i – T[i], 我们并不需要实际check该起点后T[i]个字符,我们继续从W[T[i]]开始搜索。(参见上面第6步描述)。

 

为什么不需要check前T[i]个字符?

S[m + i] != W[i]中断比较,而前面的字符是相等的,即S[0…m+i-1] == W[0…i-1],在W中 W[0…T[i]-1] == W[i-T[i]…i-1],所以有 S[m+i-T[i]…m+i-1]== W[0…T[i]-1], 进而推出,比较从S[m+i]、W[T[i]]开始。

 

以上算法 ,可以用于查找匹配的位置,那么下一步需要解决T如何求取。

T[i]表征的含义:当S[m+i]与W[i]不相等时,再次比较的发起位置。

1.    W的位置i,前面i个字符中存在最长首尾匹配字符串,假设长度为k,如果k==0表示没有最长首尾匹配串。

2.    W[i]与W[k]不相等,则T[i] = k。对于i+1时,需要找到一个新的k,使W[i]与W[k]相等,即找到最长首尾匹配串。由于W[0, k-1] == W[i-k,i-1],且T[k]是可能满足最长首尾匹配串的可能的取值,所以令k = T[k],继续迭代,直到W[k] == W[i]或k< 0;

3.    如果W[i]与W[k]相等,则T[i] = T[k]

 

举例:


i = 0时,T[0] = -1

i = 1时, k =0, W[1] == W[0], T[1]=T[0]=-1

i = 2时, k =1, W[2] != W[1], T[2] = 1,  未找到以W[2]结尾的最长首尾匹配串,k = -1

i = 3时,k = 0,W[3]!=W[0], T[3] = 0,未找到以W[3]结尾的最长首尾匹配串, k = -1

I = 4时,k =0, W[4] == W[0], T[4] = T[0] = -1

I = 5时,k =1, W[5] == W[1], T[5] = T[1] = -1

I = 6时,k =2, W[6] != W[2], T[6] = 2,未找到以W[6]结尾的最长首尾匹配串,k = -1

I = 7时,k =0……T[7] = 0

I = 8时,k =0……T[8] = 0

I = 9时,k =0, W[9] == W[0], T[9] = T[0] = -1

I = 10时,k =1, W[10] == W[1], T[10] = T[1] = -1

I = 11时,k =2, W[11] == W[2], T[11] = T[2] = 1

I = 12时,k =3, W[12] == W[3], T[12] = T[3] = 0

I = 13时,k =4, W[13] == W[4], T[13] = T[4] = -1

I = 14时,k =5, W[14] == W[5], T[14] = T[5] = -1

I = 15时,k =6, W[15] != W[6], T[15] = 6;

T[6] = 2, W[2] == W[15], 所以k = 2

I = 16时,k =3, W[16] != W[3], T[16]=3,

T[3] = 0, W[0] = -1 != W[16], 未找到k使W[k] == W[i], k = -1;

I = 17时,k = 0         

代码实现

 
 
public class KMP {

private char [] S ;
private char [] W ;
private int m ;
private int i ;
private int [] T ;

KMP ( char [] S , char [] W ) {
this . S = S ;
this . W = W ;
m = 0 ;
i = 0 ;
T = new int [ W . length + 1 ];
}

public int find () {
generateT ();
while ( m + i < S . length ) {
while ( i < W . length && S [ m + i ] == W [ i ]) {
i ++;
}
if ( i == W . length ) {
return m ;
}
m = m + i - T [ i ];
i = T [ i ] < 0 ? 0 : T [ i ];
}
return - 1 ;
}

public void generateT () {
T [ 0 ] = - 1 ;
int cnd = 0 ; // the zero-based index in W of the next character of the current candidate substring
int pos = 1 ; // the current position we are computing in T
for (; pos < W . length ; cnd ++, pos ++) {
if ( W [ pos ] == W [ cnd ]) {
T [ pos ] = T [ cnd ];
} else {
T [ pos ] = cnd ;
// prepare for next pos
cnd = T [ cnd ];
while ( cnd >= 0 && W [ cnd ] != W [ pos ]) {
cnd = T [ cnd ];
}
}
}
T [ pos ] = cnd ;
}

public static void main ( String [] args ) {
char [] S = new char [] { 'A' , 'B' , 'C' , ' ' , 'A' , 'B' , 'C' , 'D' , 'A' , 'B' , ' ' , 'A' , 'B' , 'C' , 'D' , 'A' , 'B' , 'C' , 'D' , 'A' , 'B' , 'D' , 'E' };
char [] W = new char [] { 'A' , 'B' , 'C' , 'D' , 'A' , 'B' , 'D' };
KMP kmp = new KMP ( S , W );
System . out . println ( kmp . find ());
}

}

参考资料:

https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm

http://www.ituring.com.cn/article/59881

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值