数学建模社区-数学中国
标题:
KMP算法—终于全部弄懂了
[打印本页]
作者:
杨利霞
时间:
2021-8-10 16:52
标题:
KMP算法—终于全部弄懂了
KMP算法—终于全部弄懂了
! Y2 j1 J3 l, E2 b* _( }5 f2 `
简介
& I0 s8 O: a! ^8 y0 i" E3 \0 h
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
$ i+ U2 Z, C' [* w" }
5 q9 e- N6 s0 A: W, w
" ?+ D& q, k7 @, K4 ?% B/ [: N
提取加速匹配的信息
1 {9 l+ u7 }- T8 p+ k. i' J! J
上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!
" w, u7 k0 D" l0 H8 b/ R
这种信息就是对于每模式串 t 的每个元素 t j,都存在一个实数 k ,使得模式串 t 开头的 k 个字符(t 0 t 1…t k-1)依次与 t j 前面的 k(t j-k t j-k+1…t j-1,这里第一个字符 t j-k 最多从 t 1 开始,所以 k < j)个字符相同。如果这样的 k 有多个,则取最大的一个。模式串 t 中每个位置 j 的字符都有这种信息,采用 next 数组表示,即 next[ j ]=MAX{ k }。
+ T0 K/ m7 t' w! \
* f T. J* K9 N8 Q
加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……
. h, z2 v y* s9 a
( E; I" u! P5 L+ h, x9 [+ q$ x9 Z0 ^" w2 r
先上代码
3 }$ `" H9 ?; A) x& a0 D+ \
0 U1 Y" o+ Y! }* c- `/ Z
: C$ l% C b: \8 `. O
void Getnext(int next[],String t)
6 G3 A% h& E0 P' u
{
4 z: T1 u, i* s. i' x: w
int j=0,k=-1;
! m4 A4 l3 d* h Z* ~
next[0]=-1;
b& y1 p( g+ L
while(j<t.length-1)
: w9 B6 i/ S( v
{
/ ]+ H6 p6 K: n8 A. [( ~
if(k == -1 || t[j] == t[k])
H+ F: J3 C+ N) `/ Q0 O
{
+ K1 Z5 G. ?0 F* @5 V
j++;k++;
' ^3 e2 v) D9 p8 b! } m" o! r
next[j] = k;
: _9 d5 P0 V6 k6 D
}
+ N- x2 H/ d4 X) U
else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!
/ h/ F4 c+ L( q G
}
) f5 [9 t9 t5 W) ` j
}
! m' W+ @" j- \5 z0 v2 M3 Q
* X( X5 X9 Q" K8 m
ok,下面咱们分三种情况来讲 next 的求解过程
% T* a* P; A$ a5 D
6 O8 i# Y# Z! u2 y/ |. d7 E7 a
7 e- T- p# q4 ^2 E. q
特殊情况
; ~6 ` z- b$ i
当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。
5 m( L; V6 U, I# A% O7 `. O+ g
: ] P! c7 m4 W! x' S! a
$ `! y8 S# S/ O, ^7 e- {
当 t[j] == t[k] 的情况
7 u- t- P0 I$ W4 ?
举个栗子
0 l5 v& N# S) j; S M
2021-8-10 16:46 上传
下载附件
(65.97 KB)
8 \0 [, [ E2 m, m# S1 z
观察上图可知,当 t[j] == t[k] 时,必然有"t[0]…t[k-1]" == " t[j-k]…t[j-1]",此时的 k 即是相同子串的长度。因为有"t[0]…t[k-1]" == " t[j-k]…t[j-1]",且 t[j] == t[k],则有"t[0]…t[k]" == " t[j-k]…t[j]",这样也就得出了next[j+1]=k+1。
+ r- g4 d B! c W% ^$ h
, p- ?4 I! S6 g. N- Y( B
: m4 ~8 |. z: h8 |- k1 w _$ S& A
当t[j] != t[k] 的情况
0 V- H6 w0 g i" \6 x& m. V
关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];。我当时看了之后,感觉有点蒙,于是就去翻《数据结构教程》。但是这本书里,对于这行代码的解释只有三个字:k 回退…!于是我从“有点蒙”的状态升级到了“很蒙蔽”的状态,我心想,k 回退?我当然知道这是 k 退回,但是它为什么要会退到 next[k] 的位置?为什么不是回退到k-1???巴拉巴拉巴拉…此处省略一万字。
& y) E) @; _$ R9 ~3 ^- L; f. x7 t& D. ^
, |2 d6 j) p: f8 I# }4 F0 b M4 p
5 { _2 @2 W* c h/ e: P1 X& L* g
我绞尽脑汁,仍是不得其解。于是我就去问度娘…
' z3 S$ M2 [. V- ?2 v8 U
在我看了众多博客之后,终于有了一种拨云见日的感觉,看下图
: e! P, e. o: l! t2 r
2021-8-10 16:48 上传
下载附件
(9.22 KB)
( x1 _2 `( k6 |' F3 k* g' Z- h
由第2中情况可知,当 t[j] == t[k] 时,t[j+1] 的最大子串的长度为 k,即 next[j+1] = k+1。但是此时t[j] != t[k] 了,所以就有 next[j+1] < k,那么求 next[j+1] 就等同于求 t[j] 往前小于 k 个的字符(包括t[j],看上图蓝色框框)与 t[k] 前面的字符(绿色框框)的最长重合串,即 t[j-k+1] ~ t[j] 与 t[0] ~ t[k-1] 的最长重合串(这里所说“最长重合串”实不严谨,但你知道是符合 k 的子串就行…),那么就相当于求 next[k](只不过 t[k] 变成了 t[j],但是 next[k] 的值与 t[k] 无关)!!!。所以才有了这句 k = next[k],如果新的一轮循环(这时 k = next[k] ,j 不变)中 t[j] 依然不等于 t[k] ,则说明倒数第二大 t[0~next[k]-1] 也不行,那么 k 会继续被 next[k] 赋值(这就是所谓的 k 回退…),直到找到符合重合的子串或者 k == -1。
4 o. Q, B6 }, @/ [+ z
1 q5 z5 `& H8 G- T* f3 X6 h
: G: H! J* w5 a G6 a+ I
至此,算是把求解数组 next 的算法弄清楚了(其实是,终于把 k = next[k] 弄懂了…)
* ~' H+ R5 A) y' O, P
; u4 [7 Y: o& L3 Y5 h2 @
4 W# ^: v/ I" t' K5 a5 X9 ]
因为这个算法神奇难解之处就在k=next[k]这一处的理解上,网上解析的非常之多,有的就是例证,举例子按代码走流程,走出结果了,跟肉眼看的一致,就认为解释了为什么k=next[k];很少有看到解释的非常清楚的,或者有,但我没有仔细和耐心看下去。我一般扫一眼,就大概知道这个解析是否能说的通。仔细想了三天,搞的千转百折,山重水复,一头雾气缭绕的。搞懂以后又觉得确实简单,但是绕人,烧脑。
+ }" V1 [, g! r) Q7 Y. \
9 n9 n3 y0 }3 ]+ ]0 e
, H0 k8 e4 F* F7 i* W4 S
再此特别感谢昵称为“sofu6”的博客园主,正是他的博客,让我这愚笨的脑袋瓜开窍了
/ Q) w2 J7 a' y
$ c3 n) t5 [- U0 ]* I/ q
1 [) ^) k1 u7 t. ^0 \9 M
KMP算法实现
& N' `+ @' y+ w# {$ l3 e( Z. e
当你求出了 next 数组之后,KMP 算法就很轻易搞定了,下面我用三张图,让你明白 KMP 算法完成匹配的整个过程。
6 z: r; |0 F5 h; o, b
以目标串:s,指针为 i ;模式串:t 指针为 j ; 为例
( I5 _. u/ W: G% j8 V; r9 g
2021-8-10 16:49 上传
下载附件
(68.24 KB)
$ @. _8 l& }2 X* I
上图表示:“si-j ~ si-1” == “t 0 ~ t j-1”,s i != t j(前面都相等,但比较到 t j 时发现不相等了)且next[j] == k。
: ?' C% B( O' K5 j6 c+ N, x4 ^
2021-8-10 16:50 上传
下载附件
(68.65 KB)
" D" s7 Y- ]/ M# d a+ A
根据 next 数组的定义得知 “t k ~ t j-1” == “t 0 ~ t k-1”,所以 “t 0 ~ t k-1” == “si-k ~ si-1”
: Z- e9 e( a1 |! B& ]: u
2021-8-10 16:50 上传
下载附件
(46.67 KB)
4 n: c. `7 R6 Z; I
将模式串右移,得到上图,这样就避免了目标穿的指针回溯。
& q9 _/ I7 Z& h/ q% Z
; }( ~2 [4 ?7 F$ B9 s5 ]$ S# r3 q* ]
) _; O/ U1 X) j! c# s5 Y
都明了之后就可以手写 KMP 的代码了
/ e. f! c v# v' b5 z, x; v3 ?
, W- F1 w! [! w, ?5 X3 t" `' F
) C- ^) e9 ]1 A
int KMP(String s,String t)
! f! i0 a% g6 H' Z# N0 T5 u
{
0 j* U' m1 O& h( @0 n1 V
int next[MaxSize],i=0;j=0;
4 N1 P: I2 c4 w, |! T8 Q6 @1 }
Getnext(t,next);
. R4 W. U' t' F: f C- O
while(i<s.length&&j<t.length)
, B" u! Q, A: N1 j+ |3 E4 R
{
) R! A1 }! }9 {; n6 S0 z
if(j==-1 || s
==t[j])
; {1 {% r0 J$ t4 Q
{
3 ]- m% d. r z0 O1 _
i++;
4 V; V/ @ ]& ^7 W- y1 W
j++;
2 L/ ]6 j- [# z$ D, H
}
' A( g, {" T# T2 O; o n% [
else j=next[j]; //j回退。。。
* o j) n4 i; ?
}
: k; @ C6 z; x Y# K% k& b( Q
if(j>=t.length)
- m8 z0 r+ U- y, y2 Q! j
return (i-t.length); //匹配成功,返回子串的位置
- B' }0 A2 D" x8 D
else
+ D2 d8 O) U1 {6 H" s
return (-1); //没找到
r) o7 p1 M6 I' q7 D; z( C
}
2 N: L1 a8 c/ P; m# S
$ \& f& c& E2 `+ A! o+ j8 J U
改进后的 next 求解方法
l9 d2 C, T, v3 X6 e1 N0 N! ]
先来看一下上面算法存在的缺陷:
, ~& V+ q: W2 V$ K0 y6 H
2021-8-10 16:51 上传
下载附件
(9.44 KB)
1 @2 J5 Y- K% |
显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]
3 V; v# u9 `9 c- i& p, q/ S
, K; ?( b* p* H. c, Q
# g5 Z0 {& Z5 _( g6 a9 ^( K
所以下一步我们应该是把j移动到第1个元素咯:
2 F( I+ K# \; ~5 T1 O
2021-8-10 16:52 上传
下载附件
(9.42 KB)
) b" [" d. Q/ I& @* Q" c% D. d
不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。
/ W5 @, F6 Y2 N
N; b, S' X6 w& |* F6 K
5 w% F0 w8 o* a7 E" N, w. }
显然,发生问题的原因在于t[j] == t[next[j]]。
6 @3 y& t: Q: c8 @- D& g
2 M F' A' l% w$ @1 u2 @6 C$ t
?* g& l7 H1 i! J5 q. ^/ e5 F! c
所以我们需要谈价一个判断:
0 x: x' p$ v1 s$ ?
1 |, r$ K# M8 g2 f) o1 I; w a
1 {7 }+ q! u3 X% Y) ~
void Getnext(int next[],String t)
2 j# @; e9 [/ h1 E) m. l
{
+ @( T6 g" p5 J0 T4 A0 [( O
int j=0,k=-1;
: J- k1 q. s5 J a& d }
next[0]=-1;
$ T1 | d0 T7 w( p1 d/ ^' v
while(j<t.length-1)
, V$ C5 u2 R; q
{
8 X8 S5 ~5 ]2 R- c1 T. a, V1 B4 \. c ?
if(k == -1 || t[j] == t[k])
6 k8 v* e- G* q6 f5 h
{
0 t8 E7 J3 C$ d; A; x, J: I0 {
j++;k++;
& ~( ~' Z; x5 b$ {$ Z& H. @! u. l
if(t[j]==t[k])//当两个字符相同时,就跳过
- c0 |1 E9 ]8 M' z
next[j] = next[k];
" j& o& A- }1 {( @9 j% \
else
6 k' |! L2 Z8 T/ T
next[j] = k;
$ d% e- ~1 p: m9 T2 [% ~
}
. F" D# J& W8 Z. x
else k = next[k];
, P5 y% D; @! G
}
2 k1 ?! g/ A( [' {
}
# N1 _& ^" e$ Z" r* ^
————————————————
- `7 t( Z6 V j( e2 H! I! v- u
版权声明:本文为CSDN博主「June·D」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
* l+ ?" |4 S- K5 h
原文链接:https://blog.csdn.net/dark_cy/article/details/88698736
/ ^8 J+ q' c4 r5 V8 C7 X- X
) O5 V2 L; c, _$ K4 H* _6 u: k
8 c# |: X% L3 V" j! p6 Q) \
作者:
1051373629
时间:
2021-8-17 17:11
zanzanzanzanzan
! I9 a! R7 o9 V( ^0 E `2 @9 y" t. _
欢迎光临 数学建模社区-数学中国 (http://www.madio.net/)
Powered by Discuz! X2.5