数学建模社区-数学中国

标题: KMP算法—终于全部弄懂了 [打印本页]

作者: 杨利霞    时间: 2021-8-10 16:52
标题: KMP算法—终于全部弄懂了
KMP算法—终于全部弄懂了* N$ U/ ~5 H: Z) {9 l0 c; h
简介! ~8 }$ U9 U1 g: M' w2 ?
  KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
9 G% G/ n* a3 Y% e0 E  E1 ^; R6 m# y/ E  L( b

" L9 S7 T# W3 {2 _8 k) \$ d提取加速匹配的信息
/ V% i! z0 F! E6 p2 i  上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!
" N+ {8 v$ ]: R5 u  F! B0 _7 \7 c  这种信息就是对于每模式串 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 }。3 o7 c! y3 q% ~
  8 q) n: A: y2 j" i* ~
  加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……" @2 H3 s. D& p2 _" @. G/ x& }
  
/ R9 G2 C9 q* k" D, \8 Y$ J先上代码# d1 T8 z  Z8 n7 B8 \" W
# j7 j5 W; d: {6 B. E: D$ y
- P! K, u8 ^$ O2 s4 u9 ?- C$ b  c9 I
void Getnext(int next[],String t)4 H# y1 q$ ^5 M% J
{* p* q! h+ ]0 E; C/ h* @& U
   int j=0,k=-1;
- g' Z4 J) z6 _# i2 G6 z2 ~5 e   next[0]=-1;- O& W0 ~5 R+ I
   while(j<t.length-1)
- {+ {# Q* q  B& A5 V   {
; q! n& X+ E( h0 _      if(k == -1 || t[j] == t[k])
# }. z- y  R0 i' q- `& r; N      {9 n% ^- ]( G& Z* a$ f8 f' B
         j++;k++;6 t+ f& L, Y6 ^2 j8 |6 x$ U
         next[j] = k;
. Q* {- e1 t% I7 G" [# O      }
' P  U8 G, I( v' M- X: W3 K      else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!
: u. v6 x) r; K) _8 x8 H   }# @& \1 s& o% l, t+ `. i/ ]6 c) e
}
# v# c# W) I3 s* Z9 _7 E- y/ U: a
ok,下面咱们分三种情况来讲 next 的求解过程& M# V. L5 H1 d/ c. r$ R
: Q- |! }3 o/ k1 A- s& b! ~+ o/ M

0 r' g. U2 F2 A% ^$ O) w特殊情况# m, g# H; t2 V
当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。3 C% J9 M/ @- E4 l% C/ h3 _
  P5 Y  b3 X, Y7 H7 i/ W3 J0 C

7 [, z' R9 H  L& R( e/ A7 L当 t[j] == t[k] 的情况
" B3 q4 _$ M$ A' C0 I$ B举个栗子$ C3 R# h" [/ ?& a8 E
1111.png
0 M- K  y7 B; V9 I1 f观察上图可知,当 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。/ z; D. D, ]2 J' f4 S

' ^" R6 e6 q6 M' p

+ J/ R8 T8 S5 a9 w  Q/ W% f当t[j] != t[k] 的情况
  `! `1 t( I) L* n' |; i' x8 ?关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];。我当时看了之后,感觉有点蒙,于是就去翻《数据结构教程》。但是这本书里,对于这行代码的解释只有三个字:k 回退…!于是我从“有点蒙”的状态升级到了“很蒙蔽”的状态,我心想,k 回退?我当然知道这是 k 退回,但是它为什么要会退到 next[k] 的位置?为什么不是回退到k-1???巴拉巴拉巴拉…此处省略一万字。
% E  {9 L' X3 Y6 i+ h% N/ q$ c* i! s" b% T2 l2 @

1 s' w* Y6 i$ y  `! v2 C; x& E我绞尽脑汁,仍是不得其解。于是我就去问度娘…
/ Q3 Y; f9 D1 M7 \- j在我看了众多博客之后,终于有了一种拨云见日的感觉,看下图2 |7 Z: {5 `, a( \
2222.png & O& h1 m' g2 u1 \6 G9 M
   由第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。7 V3 M6 p- H. g$ O  D& t! c' X

4 N8 K& Q% t; j: x

; Q1 m7 M. H. T# Y9 o至此,算是把求解数组 next 的算法弄清楚了(其实是,终于把 k = next[k] 弄懂了…), `! c+ x  m6 ]' `" M

, |3 F: ^- d3 R% U5 ]* {+ [
2 m! R& I* S3 s$ E
因为这个算法神奇难解之处就在k=next[k]这一处的理解上,网上解析的非常之多,有的就是例证,举例子按代码走流程,走出结果了,跟肉眼看的一致,就认为解释了为什么k=next[k];很少有看到解释的非常清楚的,或者有,但我没有仔细和耐心看下去。我一般扫一眼,就大概知道这个解析是否能说的通。仔细想了三天,搞的千转百折,山重水复,一头雾气缭绕的。搞懂以后又觉得确实简单,但是绕人,烧脑。  e! E" J9 @/ R

0 x- }: d, b4 o1 i8 h
9 d6 O1 P; P0 M) R% o: r$ f7 ~! _5 H
再此特别感谢昵称为“sofu6”的博客园主,正是他的博客,让我这愚笨的脑袋瓜开窍了, r. X  s9 g% {
3 j1 V3 l2 C# o! T+ \

; w3 c, c7 ~% x7 LKMP算法实现
1 I, g# ]- A0 e5 ?. E- }当你求出了 next 数组之后,KMP 算法就很轻易搞定了,下面我用三张图,让你明白 KMP 算法完成匹配的整个过程。
0 J0 N& l5 O7 @8 _6 L以目标串:s,指针为 i ;模式串:t 指针为 j ; 为例
0 |. g# f. J; k9 x0 W' D$ x 3333.png
" M& C$ f( |  u: Z/ o上图表示:“si-j ~ si-1” == “t 0 ~ t j-1”,s i != t j(前面都相等,但比较到 t j 时发现不相等了)且next[j] == k。% Q' o$ p% e  D& b+ W+ g3 Q4 j+ z
4444.png 9 s! s) Q. ?, u0 J. Z4 X
根据 next 数组的定义得知 “t k ~ t j-1” == “t 0 ~ t k-1”,所以 “t 0 ~ t k-1” == “si-k ~ si-1”# D- s/ H/ n% `$ t+ r
555.png 0 ~. U: |8 Q. \; k1 c
将模式串右移,得到上图,这样就避免了目标穿的指针回溯。' u9 {& ]6 p- A
5 v6 U0 S% X- C3 f: ~! }% E4 \$ y; A

7 K% B5 @$ e5 G3 X' {' Z* s都明了之后就可以手写 KMP 的代码了
1 m! }1 q- K& F8 S
& H' n% c" h+ H/ i) T! c. U& t% W2 o+ W
5 y- B; ~5 c+ v" y$ t# t9 t! Y" L8 D! v
int KMP(String s,String t)1 O: |8 O& V7 ]) `: q
{" E% J) b; q) {' }
   int next[MaxSize],i=0;j=0;
  M( {! W9 V$ M- E) f" }   Getnext(t,next);
" D/ F0 P2 x9 ]* S% m- y   while(i<s.length&&j<t.length)8 u7 i3 j5 L. k' v& |$ ]' X1 W5 z
   {% l" G+ ~9 f( r$ f
      if(j==-1 || s==t[j])
. d0 x# K# Y4 M8 P1 i      {& O# Y& U. {$ J2 W) Y% V
         i++;& P+ |2 k, Q& J7 W6 J
         j++;' u) X$ @* ?$ t. R! ]
      }( C# D# x% i! ]. c# c7 W  {; |
      else j=next[j];               //j回退。。。* S4 C# y# j  `8 w0 J
   }
; `9 _3 m9 ]8 |$ U. \1 c# T0 f' ?   if(j>=t.length)5 d' ]: s2 ~0 i& u; s1 d9 X
       return (i-t.length);         //匹配成功,返回子串的位置
6 s3 W4 j9 F/ O% x6 z5 S- v/ h7 n   else  f! N8 G" ^; n* M" Z6 I6 B
      return (-1);                  //没找到  E8 W4 j; G: u0 D& z8 m
}, ?5 \+ Z4 ^$ |% V. E5 p, E

4 g" O4 l# @8 X7 ?1 ]9 O改进后的 next 求解方法
4 t% F$ M. l9 @4 y( E先来看一下上面算法存在的缺陷:
. j* f3 @* U8 j5 v: F8 \% Y 6666.png
4 K9 r& W! c5 j9 U( Q+ G显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]
, U+ E' C  |6 ]# K+ f! X/ O" C& W2 \
6 P2 ^: U' p" z+ F
所以下一步我们应该是把j移动到第1个元素咯:' p) y( O& H% ^
7777.png
- d) X* _, R. N% F) s$ J' r* O不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。
# a% `' Q" M: _
2 W- z8 ~* @  l

, b# e/ l0 A/ z& Z3 ?3 P3 b显然,发生问题的原因在于t[j] == t[next[j]]。- K, t7 r  O) q$ H) f
" {- a2 Q5 U: n4 s; T  n6 y
, A" ~) k6 O% v4 N
所以我们需要谈价一个判断:% t; M& m7 W& I6 G/ J$ S
- P9 v6 X2 @4 {) n6 D- w

/ O3 E; k. m: F: l) ?* Z$ F" Mvoid Getnext(int next[],String t)
5 \% C- R; n4 B, u{) P+ N: v8 l: k4 ~- q# y
   int j=0,k=-1;
9 {1 L. W0 t. {% [   next[0]=-1;2 W2 C3 X- D0 P
   while(j<t.length-1)
) I4 e# r, W9 H) [0 r' u* h9 {# `   {
) s( i# j0 I9 N7 y2 E7 \0 I      if(k == -1 || t[j] == t[k])
: k! N8 z. y+ w3 _      {
4 y: X" }9 r' B; D$ G' g. P         j++;k++;% O$ y: g" b" ]. i+ W
         if(t[j]==t[k])//当两个字符相同时,就跳过
) k5 ~2 r$ R3 h3 n/ t            next[j] = next[k];
: O& z8 F8 Q4 o0 s/ R+ p5 X         else
/ O; u+ P2 U  w1 T7 q% f% W% N            next[j] = k;+ ^9 ]  a7 ?3 `  T
      }% w& t3 \( \( g! p
      else k = next[k];, m, B( w3 |% J. M) [& J# Q
   }
' z/ v+ V5 o& P* o& {' b7 t7 K# c}# d% h- N" _% c/ y( z+ L
————————————————& f+ F0 I+ x7 o  _7 o, ]1 d* J+ w8 p/ \
版权声明:本文为CSDN博主「June·D」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
* l: q4 h2 r7 |! Y7 M2 K原文链接:https://blog.csdn.net/dark_cy/article/details/88698736
/ @- J  c# x, `- d% Z7 G6 _: B* h6 N& y! R8 P

8 ~0 d0 J6 Z9 O. d4 i# A
作者: 1051373629    时间: 2021-8-17 17:11
zanzanzanzanzan
. f9 Y# X* v, m. e2 s' r' ]% ]+ s




欢迎光临 数学建模社区-数学中国 (http://www.madio.net/) Powered by Discuz! X2.5