QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2728|回复: 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算法—终于全部弄懂了
    0 Q$ ^" E, H: n3 S# V6 W& h简介7 Q; t5 C+ Z9 |1 m& L
      KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
    ' }" P! }8 ?+ S: x0 T4 K( @1 ]& W# X! w7 v! E, @& D
    % f: f  l% [+ v% y: q! L
    提取加速匹配的信息& k' Z1 ?+ N4 F& `/ ?
      上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!
    " \% |+ Z& B6 R6 p2 N+ 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 }。# G$ z0 }9 B* c8 L0 ?; x8 i# C
      0 Q5 q3 T0 |: y5 ^! i$ F$ _
      加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……
    , e& s7 {% l/ R  2 U/ Q& d3 X+ _  ^7 d; `
    先上代码
    ' C* X0 x8 m: Q7 n. Z3 U3 O9 e* l" ]% Q$ N  y" Z( [1 U
    4 f# E* R' n" T% R4 Z; F
    void Getnext(int next[],String t)
    7 O2 j8 g' p) ?( p{
    0 D5 b" ^- c1 w$ Y   int j=0,k=-1;
    8 Z( \( M$ w, Q5 e" X* H& p   next[0]=-1;
    + h$ r& p- M7 f; {. N   while(j<t.length-1)
    . x0 G1 v: n! q5 i   {
    / I3 U: j2 u+ \; u' `2 z2 E      if(k == -1 || t[j] == t[k])8 I9 f) [! c7 C5 Q
          {
    3 V. `0 K. q; U7 }. V2 L         j++;k++;- o- i8 P$ i# h2 [* k
             next[j] = k;; A# P4 Z& |; c6 A& I* s% ^3 y
          }$ r+ b1 J3 P. \  h+ \7 F
          else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!
    8 r! V' S, q( L( J, n4 a7 |   }4 l  `. i' y7 b
    }, M, v6 H$ z' ~5 y! c

    ; k/ [, L% T, {# v. q1 a  d# e! i$ Zok,下面咱们分三种情况来讲 next 的求解过程) U5 Z0 M$ _8 M3 @

    1 [$ F" t, P# z0 m5 V* r* q! U

    ( m# o3 v* C9 V+ m: f! a' @* e' A# v特殊情况! F9 |5 g2 _1 l- X9 z, m
    当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。, k* T9 v8 Y* a) G0 e

    5 U0 }% i( u  |. W1 v8 W
    6 x0 N0 i5 t3 f3 G- k6 s7 S
    当 t[j] == t[k] 的情况
    ' ~4 @& ~: q+ y0 e6 N举个栗子3 t7 T, f1 V1 Y' U; w
    1111.png
    ' t. a$ s  ]% Q+ O观察上图可知,当 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。2 G5 d" Q6 V2 C7 w
    " P8 R# D$ P+ T; X
    9 C& p& {( J: t! ?$ h, E
    当t[j] != t[k] 的情况
    ( _2 l9 `# N2 |, o" \2 X1 O关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];。我当时看了之后,感觉有点蒙,于是就去翻《数据结构教程》。但是这本书里,对于这行代码的解释只有三个字:k 回退…!于是我从“有点蒙”的状态升级到了“很蒙蔽”的状态,我心想,k 回退?我当然知道这是 k 退回,但是它为什么要会退到 next[k] 的位置?为什么不是回退到k-1???巴拉巴拉巴拉…此处省略一万字。3 p$ R2 e. @2 C1 t% [

      \6 ?7 X* j; n, T( F

      g/ P$ b: ^9 \我绞尽脑汁,仍是不得其解。于是我就去问度娘…
    ( Y7 A  I/ e& W, b8 h* ^在我看了众多博客之后,终于有了一种拨云见日的感觉,看下图9 f6 }+ S# R" @7 ^
    2222.png ; w2 E% K8 D0 U& U5 n
       由第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。% a+ m  F- D& i3 U$ s% M

    4 f5 D' H; @# W& r. i, S
    ) ?5 l- P2 i& }9 n- A9 k
    至此,算是把求解数组 next 的算法弄清楚了(其实是,终于把 k = next[k] 弄懂了…)
    & J! Y, W) ?% Y/ I" [* W
    # T8 a. s/ M/ M$ q/ g0 `

    ) j) @' A! C1 y' h) K, M! }因为这个算法神奇难解之处就在k=next[k]这一处的理解上,网上解析的非常之多,有的就是例证,举例子按代码走流程,走出结果了,跟肉眼看的一致,就认为解释了为什么k=next[k];很少有看到解释的非常清楚的,或者有,但我没有仔细和耐心看下去。我一般扫一眼,就大概知道这个解析是否能说的通。仔细想了三天,搞的千转百折,山重水复,一头雾气缭绕的。搞懂以后又觉得确实简单,但是绕人,烧脑。9 a# B7 @( P" B- u2 S
    ) T& J) r' R" a" l+ W

    * p& g6 k) z6 ~  k4 H' T! m' \/ a& k& N再此特别感谢昵称为“sofu6”的博客园主,正是他的博客,让我这愚笨的脑袋瓜开窍了
    . w  S& o8 P6 {- y2 K. f& l$ r! M. R& q4 p2 ~( D9 W3 d3 r- ~
    # W9 i- k  D7 f: E9 |
    KMP算法实现
    ; T# V" y$ h* S& g1 ^' i% t+ x7 l当你求出了 next 数组之后,KMP 算法就很轻易搞定了,下面我用三张图,让你明白 KMP 算法完成匹配的整个过程。
    * N! u. j/ R$ M; }  v- ^以目标串:s,指针为 i ;模式串:t 指针为 j ; 为例: e- T* S. v& t7 q. f3 c
    3333.png - A- z1 C5 c$ I+ g' ^% A2 S
    上图表示:“si-j ~ si-1” == “t 0 ~ t j-1”,s i != t j(前面都相等,但比较到 t j 时发现不相等了)且next[j] == k。
    9 c- F4 s" `5 U/ T$ n 4444.png " A6 i( o+ I) F+ a3 F1 q
    根据 next 数组的定义得知 “t k ~ t j-1” == “t 0 ~ t k-1”,所以 “t 0 ~ t k-1” == “si-k ~ si-1”
    0 p4 N8 s0 \- C  w, g& g 555.png # N* i7 @, \- e4 ]  ^
    将模式串右移,得到上图,这样就避免了目标穿的指针回溯。  e* {/ I7 `6 _7 y' g
    0 u! c- U6 ~5 I* f0 H& I
    + L! ^# e: ~8 j" O$ U
    都明了之后就可以手写 KMP 的代码了9 a, z" W& j% T1 @- J3 n! C
    ' g4 f0 @/ y# `# `8 q' B8 E
    3 @% C, a+ h3 b& R8 I
    int KMP(String s,String t)( ~) t/ Y4 }  H- J6 z1 ^4 k4 T
    {5 X5 B' [; M! {4 u, K$ h5 U% H$ S
       int next[MaxSize],i=0;j=0;. B2 r$ c  n! q& ^& H$ n
       Getnext(t,next);1 d% P6 j: A$ S" [
       while(i<s.length&&j<t.length)  a1 q+ |, z3 k5 `. B. M( i
       {4 A' K  t. k% R6 I3 |& e
          if(j==-1 || s==t[j])
    / V9 N' Y( O) @* T6 _      {
    / S3 ~- {$ Q- n$ A7 I         i++;
    ( B' K, |1 n" Y" I* F9 W         j++;
    : y  _" Q$ S% Z0 H+ B      }
    5 V3 L, d+ A6 a$ G      else j=next[j];               //j回退。。。; l' W. g. Z2 I# \6 l  L( l5 Z
       }5 |; m+ ^1 j5 X# {
       if(j>=t.length)
    " f1 X* c) y1 Y* C( W       return (i-t.length);         //匹配成功,返回子串的位置
    : W! b: w4 r4 b   else
      }1 ?) g& q4 p1 ^7 \5 N      return (-1);                  //没找到* h$ h# z/ q3 T
    }
    % s5 j8 ]  }% O) l& M+ l
    $ J( L, E4 u3 m! C& O+ A( f改进后的 next 求解方法
    0 Z2 o8 C0 L" Q" ]; Z- Z先来看一下上面算法存在的缺陷:) s$ M# {+ h1 C! Q  P! i" [) X2 k
    6666.png ' C0 U7 a$ m8 |9 ~5 w
    显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]0 r* l$ g: N) J) E3 a

    1 \' I( m5 c9 C5 I

    8 m. S8 @# b  D4 z: n" X所以下一步我们应该是把j移动到第1个元素咯:& {- R3 }, m& m- s( P  n: E
    7777.png
    2 B" S" p8 Z$ v. q5 ?不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。
    4 w6 T7 }: K0 {# g0 U% e
    + ?6 G& t* n2 h9 m+ i; F2 d$ F
    9 G, ^) Y" G- Y7 g2 U+ P
    显然,发生问题的原因在于t[j] == t[next[j]]。
    / K+ d6 |8 j6 u& C4 p1 X# W% z9 b) F$ j  H% A
    % ^0 T9 _- \0 g1 Q( v  {
    所以我们需要谈价一个判断:! v; K* \/ E/ N$ h7 I# |0 g
    / F# @2 F; ]% P7 Z

    3 A; ]3 N6 f' P8 Fvoid Getnext(int next[],String t)3 o5 t2 m/ Y( h- u! a. t% E
    {
    # x, t% n. q8 j+ [2 T   int j=0,k=-1;3 W4 ^0 q- T+ ]1 u& [
       next[0]=-1;
    3 Q5 p+ q* Z9 Y1 t3 Y   while(j<t.length-1)$ D* s$ A6 O! f1 ~# r% b- D! C
       {, L6 f) ~  V0 p3 a) e* z- b
          if(k == -1 || t[j] == t[k])
    / S6 b' F) N5 d3 B$ [      {, e  L5 l2 m* a" Z9 T- F
             j++;k++;( J: V  y" z' q0 O
             if(t[j]==t[k])//当两个字符相同时,就跳过
    % A6 @& c2 }# y3 }# n' U$ |: c            next[j] = next[k];; L' A5 z% }) L
             else: ]1 |- C" x0 F# P
                next[j] = k;4 N! e7 l8 I3 h% F* x* J
          }
    8 ], w7 z" ]5 J; {      else k = next[k];" g1 Q7 c1 T: x* q: ~
       }
    + p2 \& ]/ f- r; Y2 ~}
    2 j8 E" c, B" X, p————————————————
    7 N! V. n7 F5 c* x版权声明:本文为CSDN博主「June·D」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    ; \+ h( \% J9 f4 M# ]原文链接:https://blog.csdn.net/dark_cy/article/details/88698736
    3 y4 g7 Z% n' |! T- A- P3 Y- t  S, R: C5 X0 T6 o2 I
    # r6 E* V/ `  ^; T$ Q1 w
    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-6-17 10:16 , Processed in 0.513834 second(s), 60 queries .

    回顶部