QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2781|回复: 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算法—终于全部弄懂了3 s# V$ T8 k0 O: E# [# ?6 \: l  j
    简介
    0 V) i2 e1 A* _& Q* z5 A  KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。" ^& a  Z) b: D) a0 @3 u8 Q" |2 I
    - V7 d9 `$ X2 `2 ~1 j7 q1 h

    / H/ I$ V+ i8 w9 a! X0 v% N提取加速匹配的信息
    # V/ n7 }( b* k3 d9 A. k% Z  上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!
    - Z- }9 u4 [  {, [  这种信息就是对于每模式串 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 }。) M) k! ~8 ]+ }' j6 I2 [
      , q+ c4 i3 @, n
      加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……3 ]8 S. e+ Z! j
      9 [4 ~( H# D* ?9 E2 o
    先上代码8 G7 n- S+ H, q, w8 d6 Z

    ) I  D5 Z+ U/ K, f
    - Q% ^/ O9 Q% H
    void Getnext(int next[],String t)
      t' q2 q$ Q, ^5 c5 B: A{0 C2 t- k! a" C+ V. Y5 o
       int j=0,k=-1;. g2 z. h' S6 `. ~! x
       next[0]=-1;+ e1 j( @3 O. w2 v( c# x: ]
       while(j<t.length-1)
    + Z* q- G" E$ z4 R* X+ A9 h   {* w. b# j- g4 V1 q. e  n- n3 N
          if(k == -1 || t[j] == t[k])
    " g5 F" V+ a( `6 f' G      {9 Z* f* G9 v  t5 m
             j++;k++;& c4 S) o3 l6 m" e1 W& ~
             next[j] = k;
    + B6 i4 i  t* I/ Z* t' N! u% c      }
    0 B  o% L8 o+ b: J  W2 f: B      else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!1 Z% m: Y2 M& O/ @4 z7 l
       }
    $ y3 w; w1 y, d6 G4 a9 U}
    - v0 e$ k. }: \& b4 z1 ~3 |1 _% S% K+ x
    ok,下面咱们分三种情况来讲 next 的求解过程8 c7 t7 V8 t3 F% u

    . _2 H* v: `% n& {- {7 F
    . L# w7 V9 p: h6 s
    特殊情况
    ) e' l7 c) d: M: i3 C+ X当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。
    8 |+ t& T  v& |1 B8 o
    + U7 a( U$ k& ^& u6 A
    ( m$ Z5 ~. C" S( P3 n
    当 t[j] == t[k] 的情况
    8 i! m! a- T5 V; x9 ?5 C! m举个栗子
    2 e! m# J6 h# A 1111.png 1 v. |9 F& E+ V
    观察上图可知,当 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。4 T% [! o1 k5 |$ Y) Y6 @& j

      j4 O* s7 u7 a9 e+ m7 _
    , l. `' z# I; l- q: y  w
    当t[j] != t[k] 的情况
    7 Z. P( }8 n7 q+ H' C# ^$ s关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];。我当时看了之后,感觉有点蒙,于是就去翻《数据结构教程》。但是这本书里,对于这行代码的解释只有三个字:k 回退…!于是我从“有点蒙”的状态升级到了“很蒙蔽”的状态,我心想,k 回退?我当然知道这是 k 退回,但是它为什么要会退到 next[k] 的位置?为什么不是回退到k-1???巴拉巴拉巴拉…此处省略一万字。/ `/ Y! a. }! }) M* @; _
    3 F$ }( L  O% \; U$ a

    7 T3 B2 ]  m2 J2 K/ G; W  B& ^我绞尽脑汁,仍是不得其解。于是我就去问度娘…
    ( \9 e& T6 M' A. r6 O  w在我看了众多博客之后,终于有了一种拨云见日的感觉,看下图) [0 H6 @4 X* J7 [! Q7 C: v
    2222.png ; }! X! r1 a# {1 m3 N# q! K: V8 ?1 A8 t
       由第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 ]- X: g) [; [

    5 v& ?, l& ]# P
    # L3 ^& F" d5 c4 a" B/ E& K
    至此,算是把求解数组 next 的算法弄清楚了(其实是,终于把 k = next[k] 弄懂了…)3 ^5 F/ |: i' G

    * \# D6 u/ n0 o. D0 l7 F

    & d, \) @4 P' E$ `) [* Y因为这个算法神奇难解之处就在k=next[k]这一处的理解上,网上解析的非常之多,有的就是例证,举例子按代码走流程,走出结果了,跟肉眼看的一致,就认为解释了为什么k=next[k];很少有看到解释的非常清楚的,或者有,但我没有仔细和耐心看下去。我一般扫一眼,就大概知道这个解析是否能说的通。仔细想了三天,搞的千转百折,山重水复,一头雾气缭绕的。搞懂以后又觉得确实简单,但是绕人,烧脑。
      `4 M0 }1 i/ c2 _  h1 q  i
    & w% K& Z; P+ e
    5 y* P8 [' D: {; h9 O5 I4 B; h: @
    再此特别感谢昵称为“sofu6”的博客园主,正是他的博客,让我这愚笨的脑袋瓜开窍了
    % n. R8 {+ ~/ L7 f' t2 @7 m; o& r; R6 K+ g& g
    * u/ r  z& w2 t1 M, l
    KMP算法实现3 `6 i* t  W' ^* X
    当你求出了 next 数组之后,KMP 算法就很轻易搞定了,下面我用三张图,让你明白 KMP 算法完成匹配的整个过程。8 O& p5 _  t, s" N- H% W- Q* S
    以目标串:s,指针为 i ;模式串:t 指针为 j ; 为例
    % d- t* l* S, p 3333.png ( f! h9 K+ S0 ~* F
    上图表示:“si-j ~ si-1” == “t 0 ~ t j-1”,s i != t j(前面都相等,但比较到 t j 时发现不相等了)且next[j] == k。
    % s; {% d+ c1 b- D4 ^* _) i$ D 4444.png
    ( ?% r! ?) g+ \7 ]) i- l( A# U根据 next 数组的定义得知 “t k ~ t j-1” == “t 0 ~ t k-1”,所以 “t 0 ~ t k-1” == “si-k ~ si-1”; Q% V' p) K2 G% }" d
    555.png
    7 I$ p; A( M3 |; t7 c将模式串右移,得到上图,这样就避免了目标穿的指针回溯。
    / d( g9 i5 b* M& E' \: U* o7 \7 |) }7 J; z. ^6 w7 E4 m' Q

    ( K! e0 H; B# Y+ `2 h! m2 M都明了之后就可以手写 KMP 的代码了
    ( G+ W4 M/ v, C5 _& [  `8 i
    $ L4 T( T! _$ a' r8 A
    9 E7 U7 m( y9 t% @$ P. n; Y4 b
    int KMP(String s,String t)
    : T  z* M: ~  l0 ~* |{6 l& l7 H9 ^5 V* |- @
       int next[MaxSize],i=0;j=0;! P4 L" `" u. `4 ]# A  `( M  @
       Getnext(t,next);" F1 |0 q' U! Q9 Z8 q
       while(i<s.length&&j<t.length). B8 `/ u" A9 @4 r" K
       {
    , R0 \! ]1 O9 ^6 A8 n' W/ c      if(j==-1 || s==t[j])4 a$ R) J1 E; c* n4 k* T
          {
      ?+ K$ e! q* A4 i- M         i++;
    - |' D* j7 p/ U6 i) z         j++;! Z: V& y; S, Q8 x) D, r
          }4 F* L+ r# D9 h$ C
          else j=next[j];               //j回退。。。: n$ {  i& I8 h* @
       }
    $ W! I+ W( z3 J5 _   if(j>=t.length)
    " n% M5 t# l( S, T, d5 k1 F# V       return (i-t.length);         //匹配成功,返回子串的位置" b- ~3 |" E( _' U
       else
    0 G. @3 p4 p" D$ D      return (-1);                  //没找到7 @* p: C5 k$ d. S, H
    }! l, A* U% I( c2 V1 ~' T

    4 a! ?+ {$ G/ m0 I7 u, E改进后的 next 求解方法
    7 z- v, Z6 d* u8 V先来看一下上面算法存在的缺陷:
      |. [- E+ l* b, x1 W( F 6666.png
    ! u& O- S0 \8 |4 w, X3 F1 r8 Y显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]6 `% B3 v. P" n8 y
    3 b( }+ A) A  B/ K

    6 |( ^8 q9 k  z" [) F- |! m所以下一步我们应该是把j移动到第1个元素咯:
    : q1 J  h) C6 A% ~ 7777.png # J& ]3 O4 q+ D. M0 ?) h
    不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。1 d1 e' d6 _! P/ M

    * P( q$ p% |4 T. l+ X

    + Q) F! I2 b8 G1 N7 u  {0 s3 {显然,发生问题的原因在于t[j] == t[next[j]]。- s$ \2 R1 k" ~4 s

    + m! s7 r+ E1 S& j

    * O  B' B4 ^& a" A所以我们需要谈价一个判断:
    % `3 w: f; o; j5 P9 p7 b/ G7 G  j; Y/ L: s

    & u7 U+ i3 `$ J+ J: C; l& }) o' evoid Getnext(int next[],String t)+ A3 \  i2 a! n$ Z
    {" K9 B: G& R3 D
       int j=0,k=-1;
    3 e# y1 E, Y* X, L8 q6 y5 S2 l  |   next[0]=-1;
    ; C+ U" B7 `3 Z7 D  ~9 E   while(j<t.length-1)
    & r) e4 d$ ?. e# }" q% V   {
    6 T0 \5 T- f; V' g      if(k == -1 || t[j] == t[k])
    % z" d; g! H. a" t% P# ]& ^      {
    $ Z$ a4 _: |5 X  V         j++;k++;
    : N% x9 ]) ]- s8 D% ^2 s/ w         if(t[j]==t[k])//当两个字符相同时,就跳过$ [2 O3 {: \% C2 y$ S
                next[j] = next[k];$ \8 R- n1 C" H5 z  P
             else$ f6 X! h: u" y
                next[j] = k;, t( R, p8 q# C$ Z+ v! P& Q. p, R
          }
    & T3 b9 Z( @( G$ z6 L/ `      else k = next[k];+ N( b+ V) {" K6 V+ B4 W5 m3 N
       }
    + J: s! G6 `* ~+ D" t}- R3 l7 ]6 Z3 Z& K$ y) l
    ————————————————. \' ?5 o6 Y+ s( B8 D$ e
    版权声明:本文为CSDN博主「June·D」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。; a7 F& I0 I: {5 V! t. ]
    原文链接:https://blog.csdn.net/dark_cy/article/details/88698736( v- e: M3 W. h5 _6 \$ E! Z
    ; l! H; d: |* H

    ) ~# _; e1 c/ B! V. O3 K
    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-9-13 13:34 , Processed in 1.177390 second(s), 59 queries .

    回顶部