QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2704|回复: 1
打印 上一主题 下一主题

KMP算法—终于全部弄懂了

[复制链接]
字体大小: 正常 放大
杨利霞        

5273

主题

82

听众

17万

积分

  • TA的每日心情
    开心
    2021-8-11 17:59
  • 签到天数: 17 天

    [LV.4]偶尔看看III

    网络挑战赛参赛者

    网络挑战赛参赛者

    自我介绍
    本人女,毕业于内蒙古科技大学,担任文职专业,毕业专业英语。

    群组2018美赛大象算法课程

    群组2018美赛护航培训课程

    群组2019年 数学中国站长建

    群组2019年数据分析师课程

    群组2018年大象老师国赛优

    跳转到指定楼层
    1#
    发表于 2021-8-10 16:52 |只看该作者 |倒序浏览
    |招呼Ta 关注Ta
    KMP算法—终于全部弄懂了& L6 N& p% Q; |: s5 O
    简介. B+ |, u# k+ H
      KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
    - D6 Z3 R+ _* y  H; {. p* ~# s4 X: l+ v7 Y# ?& K1 ?

    9 x1 g  @/ _, D" g提取加速匹配的信息
    $ A, a; ]7 w! T4 o/ z' t- a  上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!
    ( T. x1 h3 U6 Z1 `6 T( c( `' 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 }。0 v! u: L: p$ ~6 R
      
    1 \* _9 ?* t2 v9 v% a6 z  加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……* }2 J# ~! u6 Q! j3 {* N: m7 b9 K+ E
      
    * T0 M, e; C* G' J2 `1 h先上代码* W7 r  q; b9 E- J
    # E  T! S# z& Z" a

    6 O1 @: X4 T$ ~6 ?0 `' o$ s6 Q, Cvoid Getnext(int next[],String t)) J3 B0 N9 W: A- o/ ?) M6 y+ @
    {& I8 ^- B1 G- {- m& }
       int j=0,k=-1;
    ! |4 a7 e) N! D+ M   next[0]=-1;
    & |) ^' B9 u: @3 b   while(j<t.length-1)
    + L) _" P$ \5 q$ u  S   {: _4 k( |( K0 R7 H
          if(k == -1 || t[j] == t[k])1 T" [, E" Y* J7 k7 }
          {
    6 f- R/ k6 x7 j: x6 M0 p, _4 Z& ~         j++;k++;- y+ @( r  Z. z
             next[j] = k;
    8 _5 O, {5 Y6 \( ~$ a, I      }
    2 w: X. G, u/ D: z1 u      else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!
    0 Q" p. @% q* }) F1 H% G! o$ W. G1 Z8 s   }  ^5 c, {. n  P; D
    }. q2 U9 k2 w1 I. m# ^

    1 `6 w4 s# B) `2 u; H- Hok,下面咱们分三种情况来讲 next 的求解过程
    ; ~5 F+ s: v8 b, u4 A" k9 z; t% Q
      k9 U$ j0 y3 x

    & h, ~; y4 `% U- j6 ]特殊情况7 g% f  O5 E5 u' y+ {
    当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。' T# S8 v. l2 u* _2 I

    # \" D, F: S& B% b  i4 i5 V: v* u3 ^: m

    . o% p& u6 Q" C# U当 t[j] == t[k] 的情况
    . p/ Z; l1 g2 e2 G# N% {" [举个栗子
    9 _7 o5 ~' Z, c0 g: X 1111.png
    ' c) l. R# J* ^) f& j! G观察上图可知,当 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。
    8 l0 x4 n3 U+ l& c6 c! C0 W7 F4 L3 @3 c+ l4 N8 z) y5 T6 h" {( t
    ; r1 X7 w* a; p$ y; d6 D9 d
    当t[j] != t[k] 的情况
    5 R8 Y7 j% Y8 s( C* h2 I关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];。我当时看了之后,感觉有点蒙,于是就去翻《数据结构教程》。但是这本书里,对于这行代码的解释只有三个字:k 回退…!于是我从“有点蒙”的状态升级到了“很蒙蔽”的状态,我心想,k 回退?我当然知道这是 k 退回,但是它为什么要会退到 next[k] 的位置?为什么不是回退到k-1???巴拉巴拉巴拉…此处省略一万字。# D& l- w9 x. q2 d- _" v
    * U4 t$ X9 C9 L! I  f$ [

    3 i8 M6 h' W, ?' @$ R$ W4 F' G7 E我绞尽脑汁,仍是不得其解。于是我就去问度娘…7 y5 n- S0 ^1 I* }& O$ r
    在我看了众多博客之后,终于有了一种拨云见日的感觉,看下图& W( S0 y5 b. @. c0 f' l0 y& _6 R
    2222.png
    ! e* c* x1 K8 c# J; i$ |   由第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。% h; R6 r$ h! O, y9 `

    2 e7 }  j5 p) N7 X/ M; A7 Y6 v

    + b3 d2 b4 ?0 a至此,算是把求解数组 next 的算法弄清楚了(其实是,终于把 k = next[k] 弄懂了…)
    & i5 Y1 S+ D6 F6 G, c0 h" ]* a" H/ u' v4 w" Q+ y$ t
    " S0 u6 D: o3 |
    因为这个算法神奇难解之处就在k=next[k]这一处的理解上,网上解析的非常之多,有的就是例证,举例子按代码走流程,走出结果了,跟肉眼看的一致,就认为解释了为什么k=next[k];很少有看到解释的非常清楚的,或者有,但我没有仔细和耐心看下去。我一般扫一眼,就大概知道这个解析是否能说的通。仔细想了三天,搞的千转百折,山重水复,一头雾气缭绕的。搞懂以后又觉得确实简单,但是绕人,烧脑。0 P9 y* b: X2 o9 h9 l: v" l, x
    3 y- M- y3 c/ q( Q3 ]$ K
    5 [/ A& M) R5 a  D% z/ ]9 R3 \
    再此特别感谢昵称为“sofu6”的博客园主,正是他的博客,让我这愚笨的脑袋瓜开窍了
    ! m$ G$ n$ g4 [$ ]$ p, ]
    & Y7 M4 X. K$ s# h- I1 H

    # {" H& r& ^8 {( UKMP算法实现/ I5 M. `3 P) c8 K: B0 A3 y$ V
    当你求出了 next 数组之后,KMP 算法就很轻易搞定了,下面我用三张图,让你明白 KMP 算法完成匹配的整个过程。
    2 ]/ _- a$ s8 J* P以目标串:s,指针为 i ;模式串:t 指针为 j ; 为例5 d, F+ J- _. A/ D; {# G! c
    3333.png
    1 C% P2 O1 J1 R% {7 I* n) s, {上图表示:“si-j ~ si-1” == “t 0 ~ t j-1”,s i != t j(前面都相等,但比较到 t j 时发现不相等了)且next[j] == k。3 @& Z) x! ^8 t) Y
    4444.png $ e# N' E) n9 f2 H* x' e+ j
    根据 next 数组的定义得知 “t k ~ t j-1” == “t 0 ~ t k-1”,所以 “t 0 ~ t k-1” == “si-k ~ si-1”3 [; _" B% Q. R: F
    555.png * r. q0 G9 O' @  q6 E
    将模式串右移,得到上图,这样就避免了目标穿的指针回溯。1 `+ s; e" L7 M) D

    - |& D, C) `' P8 T5 k

    4 @! m2 c' T0 @4 T1 v* @6 d$ K都明了之后就可以手写 KMP 的代码了
      m) Z; g  D" Q8 W* K% @. `+ T* ]
    - `( x8 r+ `7 d; }- G" C
    : Z# g8 z4 @. p& @" B9 i
    int KMP(String s,String t); s# w, o$ U& b' L, n
    {
    ; y. r" }! Z; R   int next[MaxSize],i=0;j=0;
    0 ?4 c# i% i3 T5 J   Getnext(t,next);  O" o' \# @9 d" x
       while(i<s.length&&j<t.length)
    ' Q0 V0 Z2 q0 S0 g   {
    4 Z8 Y. D( u$ e" _6 h' `      if(j==-1 || s==t[j])4 A. _/ V) S' W
          {1 y9 k8 Q* g3 S1 L1 O: y4 J0 C( B9 Z
             i++;
    " T0 }5 ?' T+ Y  x         j++;6 |  r  ^# a3 d+ P; ]: ~/ ^2 ]
          }
    . J3 ?' V: S6 a4 W) Y5 t      else j=next[j];               //j回退。。。5 c+ v  v( ?/ w' ^0 B. b- f
       }; E6 J3 h, d& \- R) J
       if(j>=t.length)
    ) p/ u- b7 o# e' r       return (i-t.length);         //匹配成功,返回子串的位置
    " m% U0 a' e, _7 u+ X, d   else7 j3 {1 [7 M$ K- f* N6 i
          return (-1);                  //没找到
    / u! b$ R; o4 [& K}
    6 w7 [4 d; H) [; _" B6 A: a! m+ J( {0 D* X5 P* q
    改进后的 next 求解方法
    ; ?- K8 i3 ~2 o, N7 \先来看一下上面算法存在的缺陷:
    - ?* ~* J* W' G2 _ 6666.png
    , `) u, ^8 X5 \8 k显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]( Z9 `2 v7 r0 Y( Y8 t2 W

    1 m- t$ K8 W+ W, N

    # i8 V6 o2 g$ Z7 x9 u% P8 y- G1 e$ n所以下一步我们应该是把j移动到第1个元素咯:/ l1 \( M# B9 E4 D6 I
    7777.png # H+ G+ T$ y$ [4 Y; B- X
    不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。
    3 r! {/ b. y) V7 W3 W8 v- F% q- s: A% |5 ^
    : X2 m& N9 B. Y
    显然,发生问题的原因在于t[j] == t[next[j]]。
    5 ]$ y+ ]3 _3 i5 I
    ( g  J! N7 z; r! |
    9 G# q9 i- J5 z5 N7 B+ x0 D. F
    所以我们需要谈价一个判断:
    - n  f6 J" B0 C4 j. S# k. U2 K8 V0 r
    9 S8 J; r7 Z8 m; V* i
    3 c4 K; E8 Y" Q- n; c; j
    void Getnext(int next[],String t)6 O, G4 g6 g3 q6 p
    {/ E: O4 [6 S9 _) |1 V
       int j=0,k=-1;
    5 D% |: L. W. O2 q& n   next[0]=-1;
    ; O. g  {$ q# M6 ^! y   while(j<t.length-1)" o; H+ ?; t9 A
       {1 S% t1 l& c+ o* G8 j6 ]
          if(k == -1 || t[j] == t[k])
    - m  t+ {- d# K- `7 ^: r: o      {, a" E/ X9 S0 H
             j++;k++;
    $ U: h( E- n* c         if(t[j]==t[k])//当两个字符相同时,就跳过# {1 U8 t) r3 a8 @1 r
                next[j] = next[k];$ g# B  W7 c% I7 {
             else
    3 [6 M* r* k$ a            next[j] = k;
    : A( |1 C- S1 d, z$ [      }. g5 R! ]4 D7 w7 u" O3 R+ U5 w/ g% W
          else k = next[k];) g2 x8 N  ^- F
       }5 ~0 m! r8 Q) Q
    }+ o2 a: O7 z( v6 b$ X1 G6 P
    ————————————————7 J9 r0 _3 r& `# T! @
    版权声明:本文为CSDN博主「June·D」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    9 j0 ?9 O( _3 U, `/ X原文链接:https://blog.csdn.net/dark_cy/article/details/88698736
    & |: k9 }, D( ^% T" ^# w  Z; M* \# T* o: {! h- v

    3 L+ c4 q3 r, o- p% p: O' |
    zan
    转播转播0 分享淘帖0 分享分享0 收藏收藏0 支持支持0 反对反对0 微信微信

    0

    主题

    10

    听众

    299

    积分

    升级  99.5%

  • TA的每日心情
    开心
    2023-10-14 10:28
  • 签到天数: 28 天

    [LV.4]偶尔看看III

    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 注册地址

    qq
    收缩
    • 电话咨询

    • 04714969085
    fastpost

    关于我们| 联系我们| 诚征英才| 对外合作| 产品服务| QQ

    手机版|Archiver| |繁體中文 手机客户端  

    蒙公网安备 15010502000194号

    Powered by Discuz! X2.5   © 2001-2013 数学建模网-数学中国 ( 蒙ICP备14002410号-3 蒙BBS备-0002号 )     论坛法律顾问:王兆丰

    GMT+8, 2026-5-26 11:12 , Processed in 0.420378 second(s), 59 queries .

    回顶部