QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2367|回复: 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算法—终于全部弄懂了
      j. a* L  X* m8 }1 }2 A简介  x. ~+ H( G( h6 _
      KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
    6 D; ]5 u' t) u  _1 z- C1 \* W& X- p$ ~  d0 V

    + e8 x7 _* q9 Z. d0 N& ~提取加速匹配的信息+ W/ |! Q/ d0 z( D8 ^
      上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!
    4 p9 j; m/ C0 M" z' Q6 f  这种信息就是对于每模式串 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 }。
      [. E8 N8 j$ I  
      j% n  @( s  Y2 I  加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……
    5 i5 @& R4 ]+ i8 o  
    , ]5 p7 L1 J. ~% R( [% S先上代码2 s- F% Q! g4 y9 E2 W

    ! J, E. n8 P2 b8 d' I! G
    ; n9 H* c- V9 c& z- l
    void Getnext(int next[],String t)- e  |% }7 N% e) v
    {9 ]' T6 ]0 g3 u* }* W
       int j=0,k=-1;2 L- @$ X: \% M, [
       next[0]=-1;" S$ A# B8 l; V0 Y" ?8 c4 n4 F
       while(j<t.length-1)
    # G4 Q+ |3 N& ^% _' p   {; d$ |9 J' J$ h/ M. M
          if(k == -1 || t[j] == t[k])
    , G8 z0 Y* V% L      {
    - s9 n2 L: B9 d         j++;k++;
    # }( Z! @( a( X( s4 K; E         next[j] = k;3 E2 H6 j4 g4 F, \' `0 w
          }8 f+ _8 K; ~  ?7 c8 Y) r2 g
          else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!
      E% ^2 c) j  F/ c5 U   }. ^1 ]- _* i4 f" C3 ^: U" O1 k
    }4 n. U$ O# ]# k) _+ C" \4 k. M

    ' B' `! ]; \! S% F" }0 I" Jok,下面咱们分三种情况来讲 next 的求解过程3 w1 O. C% O# `& {
    6 Z6 q8 Z  l% `' k4 }- I
    - W0 @, H) I7 t0 T' |5 w
    特殊情况
    3 Z1 H+ K# r' ^. l: z1 g7 A当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。
    0 s9 n& q  l" D4 t9 ~5 m
    & h3 D0 `' z( B. N* u! D& `

    * J& \" h3 I  B' g8 q  V当 t[j] == t[k] 的情况" {  c- z4 A" r# G0 t! `
    举个栗子
    , F. ~8 w2 r( q# ]  H" d6 M5 ? 1111.png 9 k) P6 P' z9 u$ U" s) j
    观察上图可知,当 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。
    ; B1 A5 F- L6 k1 B7 a7 j. W0 _$ j" R7 [) {2 ]
    . d/ ^; a9 i  ^
    当t[j] != t[k] 的情况
    # N6 Z! n1 ]8 \' u+ x关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];。我当时看了之后,感觉有点蒙,于是就去翻《数据结构教程》。但是这本书里,对于这行代码的解释只有三个字:k 回退…!于是我从“有点蒙”的状态升级到了“很蒙蔽”的状态,我心想,k 回退?我当然知道这是 k 退回,但是它为什么要会退到 next[k] 的位置?为什么不是回退到k-1???巴拉巴拉巴拉…此处省略一万字。' x  [; y' s. u) c2 m8 U" N( p$ H

    # n3 \- M; z* B/ S- n

    2 `6 Q6 T3 M9 F- J我绞尽脑汁,仍是不得其解。于是我就去问度娘…
    8 @! @) l5 {9 L6 c3 e. i: ]1 x3 z在我看了众多博客之后,终于有了一种拨云见日的感觉,看下图
    7 t) q+ |# T& n' B4 c& b- _* O- ~ 2222.png 9 z' a6 M( t  S8 u
       由第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。6 |! i5 \5 I& f$ ~/ N% Z

    " N% p) P7 T9 T8 Y( y8 F  e2 ?

    ) Z1 W1 J( s8 }) }. E5 B* j+ A至此,算是把求解数组 next 的算法弄清楚了(其实是,终于把 k = next[k] 弄懂了…)! b& ~7 f6 W* A; u# Y
    + K3 B" i( S* l/ z! i: o& q- b
    7 ^0 z3 `5 g1 v, F) t& t; f
    因为这个算法神奇难解之处就在k=next[k]这一处的理解上,网上解析的非常之多,有的就是例证,举例子按代码走流程,走出结果了,跟肉眼看的一致,就认为解释了为什么k=next[k];很少有看到解释的非常清楚的,或者有,但我没有仔细和耐心看下去。我一般扫一眼,就大概知道这个解析是否能说的通。仔细想了三天,搞的千转百折,山重水复,一头雾气缭绕的。搞懂以后又觉得确实简单,但是绕人,烧脑。
    , L% Y4 B$ C/ B( V& ]5 r
    0 T1 M# O2 x7 z+ U
    " s  N* a/ N5 v: i" O7 p8 L
    再此特别感谢昵称为“sofu6”的博客园主,正是他的博客,让我这愚笨的脑袋瓜开窍了% Q- o# l6 |. s& ^: Q/ I
    0 f1 z( i( ?; ]. l. a
    ; H! w5 \* ]* f- g' H. J
    KMP算法实现7 A1 T0 v4 o, {. M  _4 M" U) T, U
    当你求出了 next 数组之后,KMP 算法就很轻易搞定了,下面我用三张图,让你明白 KMP 算法完成匹配的整个过程。
    : a2 E, a, Q3 [, C1 n以目标串:s,指针为 i ;模式串:t 指针为 j ; 为例
    4 v" O7 w" a, z$ k/ I 3333.png
    6 A/ [: i$ N( L% a, u8 Z! q- t上图表示:“si-j ~ si-1” == “t 0 ~ t j-1”,s i != t j(前面都相等,但比较到 t j 时发现不相等了)且next[j] == k。. H3 n) h. ?/ ]& a1 j2 n$ k2 t' s
    4444.png
    ) Q) H; \+ H7 g( c  ~  n5 _根据 next 数组的定义得知 “t k ~ t j-1” == “t 0 ~ t k-1”,所以 “t 0 ~ t k-1” == “si-k ~ si-1”# l  l  s! _; M% w8 ^
    555.png 1 E2 h9 M% l3 d/ M) [
    将模式串右移,得到上图,这样就避免了目标穿的指针回溯。
    5 }  _8 Y. x# }, N$ l2 a5 f7 j; B" H; l

    % P/ \+ y0 P/ q' p4 o% |3 n都明了之后就可以手写 KMP 的代码了3 |, h  N2 @) N! `4 u
    6 l/ ?/ \7 C" i1 `

    8 f9 u8 W2 T  d8 a; m- C$ Pint KMP(String s,String t)
    & V1 A5 W4 x- u8 c; Q; J3 [1 p{
      S) [8 D7 n1 w6 y7 t+ w   int next[MaxSize],i=0;j=0;1 |, R$ q& a+ X* ?2 f+ h
       Getnext(t,next);
    7 t) L( t2 [5 B+ k5 q4 ^. S% Z1 P- X   while(i<s.length&&j<t.length)
    + J* u: d! j5 p   {
    8 ]+ B' x$ W6 i0 Z3 b      if(j==-1 || s==t[j])
    2 f1 f0 f; G7 i9 Z      {4 @9 E& h" z) M, }/ l' U& y
             i++;# y4 v2 q  Z. F" Q4 Z' w% G
             j++;; T: O- n! z5 s- _( U# O- r
          }
    0 I& M4 ~2 l3 @% t4 R( W      else j=next[j];               //j回退。。。
    ! r8 L' w# c5 x7 f- R" y  a   }
    ) [2 M  C, k' }. h   if(j>=t.length)
    & P* |4 \: T2 [* b$ }. U" R       return (i-t.length);         //匹配成功,返回子串的位置
    7 |2 p' R3 }: v* X! k8 I   else
    & l2 M3 C4 l9 p1 J# `- b; o9 e      return (-1);                  //没找到
    / \. K6 x' I" M0 D}: J$ r, o0 S! p$ i' N
    * ]; `6 H$ Q# @% d6 S; p1 \( ^
    改进后的 next 求解方法$ e/ \7 z* t8 \; c) Y2 Y: D" ?
    先来看一下上面算法存在的缺陷:
    $ U/ w- d2 p9 {" h+ ^ 6666.png
    " @6 h  k' S  s8 p+ \显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]
    0 o0 N' u& l$ M. R; h9 r9 P+ @# _7 H% G. D
    7 P2 a: I6 U1 f2 u
    所以下一步我们应该是把j移动到第1个元素咯:
    , T1 `  r: Z( r  {  e 7777.png
    * U: o+ T8 q! K, p0 Q4 W不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。  L' N9 S! h. g/ l

    # T+ V9 w4 K# t& ~

    % H/ L* g$ S% f% a( v. D! J显然,发生问题的原因在于t[j] == t[next[j]]。
    $ T% p! d( r9 ?
    % P8 _5 a0 v3 ?, q8 g4 a7 _+ D

    : E) K' J6 D# `, E/ G" z所以我们需要谈价一个判断:' D! T% y8 p$ y+ Q0 S3 W' r
    ( o* P1 r* }' t6 J- l! t/ }6 l8 Z

    0 r. j8 O( z4 j2 N5 K4 L- Yvoid Getnext(int next[],String t)
    1 S; U4 `. Q- N" `" x{
    & i/ l4 v- f' ^  z   int j=0,k=-1;& @) G/ L" [4 M
       next[0]=-1;3 |; @/ z. U0 z0 K/ |2 K
       while(j<t.length-1)
    0 }6 ~7 u2 z+ A2 S) x  O   {) e$ {+ a" q8 l; x1 F* D5 D
          if(k == -1 || t[j] == t[k]); {% M9 |* s& x
          {
    3 ?) `2 L! {9 @7 Z* a7 D         j++;k++;6 ^" d# t6 `0 V0 R1 V4 P( f8 [0 _
             if(t[j]==t[k])//当两个字符相同时,就跳过+ {' T0 f- E7 [
                next[j] = next[k];
    9 w+ m6 n2 i. U) e         else; @. ]2 S/ T0 ?- T
                next[j] = k;0 O8 w& V1 t* Y3 a' {7 u
          }$ L0 I+ M/ x9 l. T) v* t
          else k = next[k];( u3 Y+ i. n- K$ B# G
       }5 f. ~! z  \7 I" |3 i; I5 v
    }! F: p4 a1 R  b
    ————————————————& r% k2 ~$ x! b' n% z9 M
    版权声明:本文为CSDN博主「June·D」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    ( b7 G2 n3 D1 Z' l% V$ f原文链接:https://blog.csdn.net/dark_cy/article/details/88698736
    # Z! A3 Y8 s; s* u8 Z& F" u, Y1 Y% S+ C+ ]" y8 f

    3 F  ~* B: N0 L; S$ U5 h$ `
    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, 2025-6-23 15:05 , Processed in 0.894269 second(s), 58 queries .

    回顶部