数学建模社区-数学中国

标题: 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 mok,下面咱们分三种情况来讲 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 1111.png 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
2222.png
( 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 3333.png $ @. _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 ^ 4444.png
" 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 555.png 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 6666.png
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 7777.png ) 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% \         else6 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