QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2672|回复: 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算法—终于全部弄懂了
    9 @1 P) `9 ?6 o$ S/ s简介
    3 S/ n% B% l/ a3 I  KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
    : @: n- y, u8 g- L6 |% Q; L6 f0 h# d& B1 G3 A7 L9 z* J

    : `4 a# u) ?# g- Y3 H提取加速匹配的信息4 |3 S3 h7 n5 L* w% z9 K: a
      上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!2 W# F, x4 r1 @: c& D
      这种信息就是对于每模式串 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 }。/ v( ~8 S; m/ T/ |
      
    $ W! |: s. q3 g; O  加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……
    6 l& Q* L" E& L: p. V4 G  8 J2 u7 s. D  h1 T; l, h1 H. q
    先上代码
    ) O$ ?  `0 |# g$ b8 \3 k7 V
    . _- R* J2 v  M& F  p
    . x, M. q( p' L( y& W0 \
    void Getnext(int next[],String t)
    # i" V* I/ H, T0 T{
    $ p' Z' t  z* w& E/ s- W& `   int j=0,k=-1;
    & E1 t8 b5 o  P$ G: p   next[0]=-1;/ a3 ]+ }6 o5 g4 Z
       while(j<t.length-1)# [: B1 e$ l& [. q+ f" a9 y: X5 U
       {
    0 [$ y6 b0 n5 A! a7 t8 l      if(k == -1 || t[j] == t[k])  C, L5 d' `* M. P3 n& J
          {
    ; {) d1 ~2 y1 G* }* O% P1 f& x         j++;k++;7 j* v+ _$ {  N
             next[j] = k;# y+ N: q9 b4 v9 r. A* \
          }1 s: [4 S8 q9 a; u
          else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!+ \- g# b: y0 b) f6 J% C
       }) N" O$ t6 I1 x- C2 N3 k) L
    }; _; a4 X: e! @$ D; q& ~

    3 G0 `9 I, e3 @ok,下面咱们分三种情况来讲 next 的求解过程
    2 P7 B- D9 B! {- ^+ O) H' K
    8 T; t2 e, [; w# r7 z/ r  F

    2 E6 Q9 V4 y) C特殊情况
    6 s' g4 F- K/ {当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。
    3 A$ H# D1 E+ w# g* n( M" ~% N5 ~: F8 M9 d0 p
    ' v+ B2 `9 [- ?0 ]9 X3 a/ N  v
    当 t[j] == t[k] 的情况0 ~# U0 o! a0 [* b% b
    举个栗子
    ( \$ g+ V! \8 k6 M' L/ v  ]/ e 1111.png
    # B  D( ~* D# K$ _( ?% l( L2 Y观察上图可知,当 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。7 n, K  o7 Q- S' ?

    4 `/ g" ?6 [8 M* ~4 H& Z

    6 b' ?+ N9 z- Y当t[j] != t[k] 的情况/ O2 Z, W: j/ Z4 S7 |
    关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];。我当时看了之后,感觉有点蒙,于是就去翻《数据结构教程》。但是这本书里,对于这行代码的解释只有三个字:k 回退…!于是我从“有点蒙”的状态升级到了“很蒙蔽”的状态,我心想,k 回退?我当然知道这是 k 退回,但是它为什么要会退到 next[k] 的位置?为什么不是回退到k-1???巴拉巴拉巴拉…此处省略一万字。
    # X8 R1 j* D+ }5 R7 F- Y# M+ b0 w5 O8 E3 M
    ) x; P  h1 h2 N
    我绞尽脑汁,仍是不得其解。于是我就去问度娘…' q  T& W1 @. t+ c& F
    在我看了众多博客之后,终于有了一种拨云见日的感觉,看下图
    $ A: {" S7 E% N 2222.png
    1 r  E4 G4 p) n5 n* @9 l4 x   由第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。2 T6 p' g& d4 ]: U

    * `( ?  a5 ]5 o% A) h$ i* h" C
    8 r6 Q# C$ R7 o$ L5 F/ Z, U
    至此,算是把求解数组 next 的算法弄清楚了(其实是,终于把 k = next[k] 弄懂了…)
    " C9 k9 r  m, I! B' z3 T0 U$ N- B; Q' t2 W

    1 x  f% N/ _& u8 o. r7 h因为这个算法神奇难解之处就在k=next[k]这一处的理解上,网上解析的非常之多,有的就是例证,举例子按代码走流程,走出结果了,跟肉眼看的一致,就认为解释了为什么k=next[k];很少有看到解释的非常清楚的,或者有,但我没有仔细和耐心看下去。我一般扫一眼,就大概知道这个解析是否能说的通。仔细想了三天,搞的千转百折,山重水复,一头雾气缭绕的。搞懂以后又觉得确实简单,但是绕人,烧脑。6 _+ T0 ?9 J( o, S5 p  l; Y8 g% y. i
    & A, Q1 w) g1 _

    5 [! |4 o8 v2 O, W* W: f4 L* E) R再此特别感谢昵称为“sofu6”的博客园主,正是他的博客,让我这愚笨的脑袋瓜开窍了
    & ~8 i' _* c' i* y  Z' \4 J; i" k- F' G

    ( j" W! T7 U; k7 ?. P; r! A0 UKMP算法实现
    ( u% g( y/ N9 l7 c4 U0 h0 _当你求出了 next 数组之后,KMP 算法就很轻易搞定了,下面我用三张图,让你明白 KMP 算法完成匹配的整个过程。
    - x1 S% u) c1 f% o9 V& j& n5 R以目标串:s,指针为 i ;模式串:t 指针为 j ; 为例( l3 H, ]3 ^& K% [8 |
    3333.png
    2 I) F/ m# s4 N8 }8 L- ^上图表示:“si-j ~ si-1” == “t 0 ~ t j-1”,s i != t j(前面都相等,但比较到 t j 时发现不相等了)且next[j] == k。, @. [3 |+ V: b! v; @
    4444.png - K% L7 ?3 D" E* z; ]8 S
    根据 next 数组的定义得知 “t k ~ t j-1” == “t 0 ~ t k-1”,所以 “t 0 ~ t k-1” == “si-k ~ si-1”
    & G; y+ U0 n! k: ^5 t 555.png # _  ^, m. N8 }+ V& G+ ^  V
    将模式串右移,得到上图,这样就避免了目标穿的指针回溯。# `) K6 q$ t- ]

    * [0 P, N1 I5 [/ r7 `* l

    3 U# K4 V: p& c) I0 N都明了之后就可以手写 KMP 的代码了0 T: v1 f8 q: K. d- B
    & X& B% F/ V$ l, I) U

    / ^: G0 a- T( m6 Bint KMP(String s,String t)
    & d) J* J$ v' _, s, y# c{
    $ ^$ h5 o% M7 v/ f2 T- A   int next[MaxSize],i=0;j=0;5 V8 ^9 N0 G; n& R5 I- h
       Getnext(t,next);
    ! r3 y& h9 V) u- s9 \( K   while(i<s.length&&j<t.length)0 c/ Z/ ^; D+ b- i
       {' F! m  ~& H) \5 l+ e
          if(j==-1 || s==t[j])0 {) d9 g4 X' B& _8 j( s
          {3 G6 z; ^5 o+ _& ~3 @$ I6 [
             i++;
    & @5 H7 D. I, u: X         j++;! ?; A& a$ h. N4 e/ i% o2 W; w. d+ d
          }
    % n' U  C0 v9 Z2 b: A      else j=next[j];               //j回退。。。
    . P; o* {. \, t! Z  o   }: Z: f% x% e# y6 S6 K% s
       if(j>=t.length)
    - `, V3 a8 h( C2 h/ [       return (i-t.length);         //匹配成功,返回子串的位置" P+ X4 h, P+ u; O
       else: K6 j! _' Q  t/ m/ N. N
          return (-1);                  //没找到
    , m% c6 |4 l: X% w! p}3 V! m2 ^! L- n# _! X, |2 b. |
    & k9 \. p! q2 u8 ]# K$ {
    改进后的 next 求解方法
    3 t1 ~  c# v  p5 I1 b$ s. z( F) m先来看一下上面算法存在的缺陷:$ y' C  B2 d4 b7 d' V
    6666.png
    ; k+ w) P2 X3 r4 |. |显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]
    7 l0 O% o8 O5 g
    7 \, ^3 Q: p& u0 T; W

    3 z, j8 H- U1 \9 R( E# K, }- ]( n. p所以下一步我们应该是把j移动到第1个元素咯:9 }$ N+ I2 ^: c
    7777.png
    2 e  _5 v+ F9 f/ s不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。3 I4 M) d5 v+ p- M2 e7 P% }. h1 `# i
    3 g! u9 W) q8 p$ T/ h! c0 {

    , @0 _# ^& c, i* K3 A显然,发生问题的原因在于t[j] == t[next[j]]。
    ! y1 }6 I& h' u6 E2 @/ |, |% T% Z1 E0 }, F% l7 B$ w2 \
    * c& o9 X0 I; g$ x' B7 a
    所以我们需要谈价一个判断:
    1 L' i4 c1 Y& K( {+ k: n
    & c- l5 _$ ?9 c8 u0 |/ `
    & q+ [( `# s- l4 Y0 k
    void Getnext(int next[],String t)  d+ @' \" ~: |3 `1 {
    {
    7 W6 n* A" w8 ]& o0 B8 o   int j=0,k=-1;# j5 _2 ^/ A: z# ?/ I7 ~8 i
       next[0]=-1;( P$ f  F' G9 L* i4 h2 [
       while(j<t.length-1)' ]& c# f! N2 I6 d( i$ |
       {
    6 D/ u# b  H$ T: |; T# {* a      if(k == -1 || t[j] == t[k])( n$ v) f2 \/ t6 I
          {
    6 ?, W9 O$ s6 x0 }% Z3 M* d* m1 e5 b         j++;k++;5 w$ s( o4 ^" a: a- d5 E8 r8 l
             if(t[j]==t[k])//当两个字符相同时,就跳过8 I; l( i  ^! ?
                next[j] = next[k];- e3 I& q4 K7 n& p5 c% x
             else
    + ~# s/ t% [! M( K; I( E8 E$ ~            next[j] = k;
    % I+ S' F: t: I# v      }  V2 ^* ]0 J% o) M' @
          else k = next[k];
    3 n  Q( V) Q8 C! n& m   }& Y1 a* U) O! y* Q  }, P
    }2 ~" U" ?7 m1 }: Y* W$ o6 n7 C
    ————————————————
    ; e5 m* D* s4 X, N* Z$ W9 h  P版权声明:本文为CSDN博主「June·D」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。7 Z% r8 c$ M3 E
    原文链接:https://blog.csdn.net/dark_cy/article/details/88698736" X1 J- W9 h# y# z& j2 W
    8 w8 E4 U, }4 m7 i
    . {2 }+ o* n# s% B0 [6 M
    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-4-10 17:24 , Processed in 0.444262 second(s), 59 queries .

    回顶部