数学建模社区-数学中国

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

作者: 杨利霞    时间: 2021-8-10 16:52
标题: KMP算法—终于全部弄懂了
KMP算法—终于全部弄懂了. r5 g4 D: K& P" S% V, ~
简介6 l# ?; M+ o; |6 V: O  }
  KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
$ [" T# d7 z' \# z7 H& d% p5 ^1 N* A' n* _4 t+ ]9 f
/ k7 w7 f0 ^( y" s4 d' ~6 d. L
提取加速匹配的信息9 j1 N2 F4 }/ U& K  \1 J+ r
  上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!  s0 ?" K, @8 @4 v, t5 z
  这种信息就是对于每模式串 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 }。5 d* \4 [. A" z4 l8 S9 q/ d/ A
  
. {( N! x: h& D  加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……- V  u+ P4 q* F( F1 a1 o" U8 I
  4 R9 Y$ u2 R7 ^0 B# R2 I4 X8 R
先上代码+ H' V7 Q( N$ A6 M
' g% w7 q/ j6 q2 W
# W) d$ H) a7 U" c0 O
void Getnext(int next[],String t)9 s9 u; _2 i( j3 c2 S' [
{  ~  w$ Q. i  T
   int j=0,k=-1;5 i3 f# d8 E; o0 G$ d0 x4 s
   next[0]=-1;
# y: G5 P8 A( [# L* b3 O' j7 Q$ j/ i   while(j<t.length-1)/ Q0 C! W( |3 f) e7 @
   {& b" j9 ]3 b7 C: k
      if(k == -1 || t[j] == t[k])
4 P, M; V; P: f; H% g/ w      {
! u, I, K; s5 ^$ D* G/ [         j++;k++;4 y$ B" @8 O) s5 w( H9 S3 W3 }- o- }
         next[j] = k;, V4 f+ Q) h: Y( z, l: H6 i
      }, j1 y' x  I7 Y. k
      else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!  T4 }) }/ I# x9 S
   }4 T! _+ U$ K6 v9 U' u
}
! @8 `( Q* F3 x$ ^9 o/ a
- X/ m! {4 n: T. \ok,下面咱们分三种情况来讲 next 的求解过程; G- {' d" }4 ~* R. }; Y

' B6 J' W5 u# y( r* l, Q

% `: {7 H5 _! ]- k9 a特殊情况
' `" ~% c; |8 y& h+ V当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。9 q2 k; Z5 U0 Y* |7 r

, x. z. r. [  E4 i5 ~
7 R& d9 B4 Q) J) W! Z1 L0 i# L8 I& r: n
当 t[j] == t[k] 的情况" x" Q  ?+ M4 G5 l; K3 ?
举个栗子
( ]8 g# x  e9 G7 n' K- h2 I3 I 1111.png
3 n: q& D) f! I' }+ q观察上图可知,当 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。0 b/ S' g( V+ E2 U" {+ a
( p" ^, @+ {) Z! G

, f5 S% q9 d# ~  p当t[j] != t[k] 的情况  @* a% g$ d) @( Y
关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];。我当时看了之后,感觉有点蒙,于是就去翻《数据结构教程》。但是这本书里,对于这行代码的解释只有三个字:k 回退…!于是我从“有点蒙”的状态升级到了“很蒙蔽”的状态,我心想,k 回退?我当然知道这是 k 退回,但是它为什么要会退到 next[k] 的位置?为什么不是回退到k-1???巴拉巴拉巴拉…此处省略一万字。% X" Z& u  Z1 x8 U/ b) [
% W+ I2 H: ^; [# n2 E1 o% M$ J7 ]
% P5 P  Y" N9 A9 P* y
我绞尽脑汁,仍是不得其解。于是我就去问度娘…
1 m8 K3 Y; F/ N! S: Q在我看了众多博客之后,终于有了一种拨云见日的感觉,看下图
, F* q2 _. O1 d' p# ]/ s. |4 ?- z 2222.png
/ P, M' a! g. d6 V* s% K7 d   由第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。( g: h# H3 q2 d) G3 U) K' s7 E

& u! b. K) c+ m8 b3 y4 F
6 S  k  K. l3 d8 W0 [
至此,算是把求解数组 next 的算法弄清楚了(其实是,终于把 k = next[k] 弄懂了…). L4 ~1 k: `4 p7 i2 i2 r4 f

2 `3 t+ E2 }! b7 W" F" w2 n

; o$ O4 e3 b% ]$ i因为这个算法神奇难解之处就在k=next[k]这一处的理解上,网上解析的非常之多,有的就是例证,举例子按代码走流程,走出结果了,跟肉眼看的一致,就认为解释了为什么k=next[k];很少有看到解释的非常清楚的,或者有,但我没有仔细和耐心看下去。我一般扫一眼,就大概知道这个解析是否能说的通。仔细想了三天,搞的千转百折,山重水复,一头雾气缭绕的。搞懂以后又觉得确实简单,但是绕人,烧脑。
1 D% y/ P2 E$ Z
7 M. e; I4 v& }, ?1 m7 O
! ]( i  D; I3 ^1 b$ V, X
再此特别感谢昵称为“sofu6”的博客园主,正是他的博客,让我这愚笨的脑袋瓜开窍了! ^( h1 h# A$ }2 H3 N, M( W8 K
7 M) T; j) i' d) w1 Y. p
) o; E5 D' \9 L. M/ p1 @. s
KMP算法实现* W1 t& [8 p( V7 m* x# I9 D- v
当你求出了 next 数组之后,KMP 算法就很轻易搞定了,下面我用三张图,让你明白 KMP 算法完成匹配的整个过程。) Q1 O; b+ Y6 m& I
以目标串:s,指针为 i ;模式串:t 指针为 j ; 为例
; Y4 e* v# E  b0 E( h! T 3333.png " O4 l- Q- }3 N$ ]: \3 F. g
上图表示:“si-j ~ si-1” == “t 0 ~ t j-1”,s i != t j(前面都相等,但比较到 t j 时发现不相等了)且next[j] == k。- F/ ^! {, q& y! m+ T
4444.png ' L, y. u+ |, m: A  [2 E
根据 next 数组的定义得知 “t k ~ t j-1” == “t 0 ~ t k-1”,所以 “t 0 ~ t k-1” == “si-k ~ si-1”5 o& g* t* p" N6 |/ J
555.png
' I/ G5 Y9 k# k6 k, n4 z将模式串右移,得到上图,这样就避免了目标穿的指针回溯。" A7 u2 m: R& q" h
+ Z2 i, X& S; g+ J( k

! F& v" k, X2 G6 `都明了之后就可以手写 KMP 的代码了! j2 d/ D9 f6 w) n) S" V
" q3 e9 m: f6 x) K' P. ?: [6 ^6 C

' b# x/ g, g. r# o& ]# @int KMP(String s,String t)
" Q, K* y/ d. x$ F. S# ~0 X# A' N{
# {) x/ w  n; P* s' |6 f: F" O0 R5 p   int next[MaxSize],i=0;j=0;6 ?  |9 X* }: P7 G) T7 r; X) E# @
   Getnext(t,next);
  P9 I' P: i. M8 P9 J$ U3 u   while(i<s.length&&j<t.length)% w) w6 j! V; Z/ P
   {
. w) q1 r3 N: r% p" C, m      if(j==-1 || s==t[j])5 v- f) j. F' o: m: ?2 }9 Y
      {  b& L/ \! u( ]: l
         i++;
+ j0 m9 u9 n1 {8 k' s, O         j++;
* z! h0 f( y8 l4 W. ~% N      }
( O8 E* T7 c. Q. G      else j=next[j];               //j回退。。。
0 g+ R1 J0 I* K5 B   }/ d7 H% c  i7 ~  F
   if(j>=t.length)+ ?( i7 F6 |7 ~9 n8 \. p# t7 ^
       return (i-t.length);         //匹配成功,返回子串的位置/ k: y, u/ x: u% p; {# w
   else
( a( Y7 l  \% q      return (-1);                  //没找到/ X/ ?9 s7 Z9 G$ e: w- @' V
}
8 V6 g7 G9 G( k( k6 w! T& [8 Z9 v$ U  d
改进后的 next 求解方法* c) {$ K/ J. u$ _3 y1 d8 @& L
先来看一下上面算法存在的缺陷:
0 b: |1 b4 h, |0 ?0 z6 \ 6666.png
% j# L1 e- s/ V9 Y# t显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]/ F- l& |% B& h( C  v% B0 M

3 m- }4 I  @3 I5 k* t

3 a( c7 \+ c* a+ \所以下一步我们应该是把j移动到第1个元素咯:- p7 g8 @+ y* l
7777.png
: |. ^+ E7 V( Z8 i1 F8 r7 V* {不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。
4 c& x' W3 f: C) K
) m# W. A2 ^7 A6 A5 Q

  G) ?4 v4 M: R% C5 j8 {显然,发生问题的原因在于t[j] == t[next[j]]。1 L* A3 O7 s3 K9 X  R
% k+ Z% ]( e1 I- ?% k# T
8 ]1 U. ]/ _2 |" Q
所以我们需要谈价一个判断:/ d, z' g; H$ M9 Y

3 \+ S5 [; l3 L9 E1 ~+ X( ?& b
) Z4 H) O8 {1 G" p* l
void Getnext(int next[],String t). L9 S- X& S% h3 p, N
{' P7 r& e+ T( |$ P
   int j=0,k=-1;
& |5 C: S$ i- k$ ~  Z   next[0]=-1;
; ~1 h- @% W' J9 f2 G3 G   while(j<t.length-1)
# @9 Y' j2 l+ g& Y) N   {9 |. U2 d. x7 ^( X$ d, S
      if(k == -1 || t[j] == t[k])
9 Z4 U4 c4 F) _8 I, j& `4 f      {
' T( H8 ~. ~. k% o: ]; O         j++;k++;. T% x; |% i2 I$ \
         if(t[j]==t[k])//当两个字符相同时,就跳过9 d8 P5 N7 Z+ f/ `" G2 r
            next[j] = next[k];" b/ Q( H  {, Y0 Q8 A6 ~
         else/ x; ~5 s; z0 v9 d  z" ~
            next[j] = k;
. b& ]' y+ Q7 F4 G- M: K- r. f      }8 _  p" i( w$ U; i, x4 q
      else k = next[k];$ \1 v2 a9 |5 h0 n
   }
# \# D6 [4 H2 Z5 g, ~5 Y}$ r$ }6 s4 ^$ k- v& ?9 Q; ]' n
————————————————
# E9 m) i6 y- y版权声明:本文为CSDN博主「June·D」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
& E: F; I1 H. {0 N- J原文链接:https://blog.csdn.net/dark_cy/article/details/88698736% H( \- l/ U' ~( _% X( n
7 h+ }& b  e7 ~+ P$ ^

8 G9 E9 c; n8 E* P1 c! v: f: _
作者: 1051373629    时间: 2021-8-17 17:11
zanzanzanzanzan- Q5 |# U  i& m* E" r( V) t: G





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