QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2757|回复: 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算法—终于全部弄懂了/ P& S" \' r3 ^; ?& u* z
    简介2 G* p0 ]9 ?; ?- r0 [$ k
      KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
    , [% r1 M; V! L9 L& d7 e
    ! P6 u. {+ M; l. O* p) v4 i6 z3 c- H

    , A3 g5 l9 e% p  g; A' u! X提取加速匹配的信息5 x3 }0 h1 R# t" N$ g- d
      上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!: s$ h0 Z6 n* T
      这种信息就是对于每模式串 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 }。
    7 n" A; F6 _% T5 {5 r8 L4 D  + D: K  Z# r0 [+ l" t. D
      加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……  p9 y: y$ Q7 z
      
    & l9 r( x( M; t5 x9 A% S先上代码
    + ?+ R& Z3 `" R  A8 e
    6 T/ p- R; {* N0 D) F* H  m
    8 u. k1 ?0 ~, Z! B: Z
    void Getnext(int next[],String t)2 f  \' I1 j& G! Y/ _8 u8 y
    {
    0 o1 a0 T: \' j+ A( a7 y   int j=0,k=-1;7 N" t" [2 g' F3 c3 {  _/ R
       next[0]=-1;) |* ~3 P, G) u, ]' \, Z- v
       while(j<t.length-1)
    4 v- z8 N4 h6 p# @' ^% x   {8 g: }  d/ r0 Z8 ?8 D1 d
          if(k == -1 || t[j] == t[k])
    3 V9 L- U% j) ]# I6 u' Q! n      {
    9 K; w. V6 M% |) S4 X# Y% c9 I         j++;k++;. n- p# k) F+ Y1 c' K+ G
             next[j] = k;
    ! N4 B0 C" `8 s' u# s. [7 l      }
    1 f7 p' ]! \; F" M3 {. S7 Y7 Y4 H      else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!: ~" ^! s) z! t  n+ k
       }- A& Z/ W2 ~1 [9 ~+ r
    }+ `/ l3 }; Y) l" _: b
    4 w' `; r: I/ g5 D) k* X* E" h* m
    ok,下面咱们分三种情况来讲 next 的求解过程
    - f0 L' z3 s7 A, @+ T
    * U$ q0 d. }3 J
    5 ~) ?$ h1 x1 C0 M& N
    特殊情况- q1 `: E$ E8 h
    当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。7 v( m3 f6 T' j, `- C( x! `: m
    . N6 b4 J% x3 o; n; ]
    , W% x) t, P7 g, u8 j. P
    当 t[j] == t[k] 的情况
    6 o+ F7 }3 P$ b6 [" Y举个栗子
    : \- K, V! ^; x2 t; _ 1111.png ; k2 {  w/ e- t9 L6 w% z5 p! f  ?( }
    观察上图可知,当 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。" g* ^  T' `3 o& t( Y

    4 C, {/ F" C* O8 H+ S/ ]7 V& L. ^
    : U+ q4 s7 W/ U8 E  Y
    当t[j] != t[k] 的情况
    6 d9 d$ J; ]( G关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];。我当时看了之后,感觉有点蒙,于是就去翻《数据结构教程》。但是这本书里,对于这行代码的解释只有三个字:k 回退…!于是我从“有点蒙”的状态升级到了“很蒙蔽”的状态,我心想,k 回退?我当然知道这是 k 退回,但是它为什么要会退到 next[k] 的位置?为什么不是回退到k-1???巴拉巴拉巴拉…此处省略一万字。
    # X8 x, T  _6 s- L& V6 f. q; T8 f% S( Z: n* b! l
    ) C: H5 t, j* p' W
    我绞尽脑汁,仍是不得其解。于是我就去问度娘…
    % h1 R7 u" p& c9 F9 ~: E在我看了众多博客之后,终于有了一种拨云见日的感觉,看下图
    3 q$ Z6 n: T3 {1 f- c7 l4 K 2222.png
    & K% I( I: J( |, C% j' ^2 v   由第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 H2 W9 D% z1 k3 I" G

    + _6 D8 X8 i+ ^6 X4 @

    4 b+ h. u9 X: M( m% D至此,算是把求解数组 next 的算法弄清楚了(其实是,终于把 k = next[k] 弄懂了…)) s- i( N$ |% x* N0 _- }
    ( d* y9 H, K( y% A2 |2 ]! a

    ) C& a/ b$ M' d' U) Y- n7 L( h因为这个算法神奇难解之处就在k=next[k]这一处的理解上,网上解析的非常之多,有的就是例证,举例子按代码走流程,走出结果了,跟肉眼看的一致,就认为解释了为什么k=next[k];很少有看到解释的非常清楚的,或者有,但我没有仔细和耐心看下去。我一般扫一眼,就大概知道这个解析是否能说的通。仔细想了三天,搞的千转百折,山重水复,一头雾气缭绕的。搞懂以后又觉得确实简单,但是绕人,烧脑。0 v) s5 }2 ?( x- x3 y; T' z

    6 S* ~2 q+ c' l2 V! K% [* L
    7 ?; V2 |% I- m' r
    再此特别感谢昵称为“sofu6”的博客园主,正是他的博客,让我这愚笨的脑袋瓜开窍了0 E" I8 t" ^* @- W. A

    . O9 L: Y  n5 j5 }1 O2 y

    , ~$ v( \  S& C' g3 d/ J* G; i8 NKMP算法实现
    0 I7 L% S' E: H$ G& e, T, y& `当你求出了 next 数组之后,KMP 算法就很轻易搞定了,下面我用三张图,让你明白 KMP 算法完成匹配的整个过程。
    0 D& Z& }. b: j以目标串:s,指针为 i ;模式串:t 指针为 j ; 为例& i7 C7 G7 y8 A8 O- i
    3333.png
    . C* P  I' U9 [- N. B2 k* S上图表示:“si-j ~ si-1” == “t 0 ~ t j-1”,s i != t j(前面都相等,但比较到 t j 时发现不相等了)且next[j] == k。  G& |/ d! _2 Q( Q$ m
    4444.png . U8 j: s3 @- l8 F- O3 D
    根据 next 数组的定义得知 “t k ~ t j-1” == “t 0 ~ t k-1”,所以 “t 0 ~ t k-1” == “si-k ~ si-1”
    # J/ x* g- B: i; }- u 555.png / F+ N* @1 ?) x7 x4 T. U
    将模式串右移,得到上图,这样就避免了目标穿的指针回溯。
    # Z# P9 }/ S2 R, ^% W2 x9 i7 U; h5 V. _' H

    : b9 G  ^# W4 [# H2 Y/ T# r  b都明了之后就可以手写 KMP 的代码了6 e! @2 @  N, O/ Q

    2 V% a7 Z) p$ ?$ Q, Z

    " a0 h9 ^1 O, }  |/ X- Eint KMP(String s,String t)( @# z8 m! F' E9 L9 K3 W
    {+ m2 O' ?. x. E% k8 I$ H7 X; x
       int next[MaxSize],i=0;j=0;
    ' k9 c2 q; P% U2 p. w0 o   Getnext(t,next);
    ! I, d& E; f( I   while(i<s.length&&j<t.length)
    4 v9 |8 K7 f& i   {
    * S  e- f: T) w& T      if(j==-1 || s==t[j])5 C* P0 `; U! C. a( f1 Z
          {
    : d- T8 q2 r' ]         i++;
    7 y+ H: r$ x9 E; w         j++;
    ; B$ k4 P, H' ~      }
    2 W* |4 f$ A$ q5 W      else j=next[j];               //j回退。。。8 k5 X0 g( h) F
       }. l: I" y; `8 {" C; E: I& t
       if(j>=t.length)
    , G" K" K$ o/ f3 e0 g+ G       return (i-t.length);         //匹配成功,返回子串的位置
    ( U, [6 c7 a$ d1 o) l+ @3 v   else% n3 y3 ?' w* n# l; S
          return (-1);                  //没找到
    6 r# R! \) f3 Q/ n* F0 K}$ v, q4 V5 h; H$ w. K

    8 [- C; e9 ]4 Y/ {: C+ o改进后的 next 求解方法
      G4 O& |3 ^7 l; n+ {先来看一下上面算法存在的缺陷:
    5 z7 m6 |* A! T! _/ X2 O 6666.png
    - t2 J' N, X- E- P! ~显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]
    1 V6 x. R7 y7 F# p- _1 }0 M% E" {

    ) a" G* }. x, @% a" _( F所以下一步我们应该是把j移动到第1个元素咯:4 V& M! G+ J; x6 v. h+ L; n
    7777.png 7 x2 i; \' @; R4 ?1 L' w7 P
    不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。' q4 C$ o) \' B( `% Y9 e

    + d( I& {( \: P* H5 {7 ^
    / Y( N7 r7 @6 A
    显然,发生问题的原因在于t[j] == t[next[j]]。) v9 _  `% v$ G# u3 O

    2 @2 x  q$ U5 [% b9 k9 W2 K2 \  B; O2 Y

    " I5 G8 w0 v* {* p/ ?* m所以我们需要谈价一个判断:0 p. ^0 t/ w# D& q0 q8 T
    2 o) M9 M9 c$ g- y) ]) P

    3 v% k7 O: l' D# t8 R# w  Q" qvoid Getnext(int next[],String t)
    ) O" {, h9 o4 M" q7 a/ p$ \6 C8 l+ l: L{
    + }' e% G. \0 R1 R8 ^% E   int j=0,k=-1;* u! C+ a9 F1 F8 g
       next[0]=-1;
    # e0 v4 G' Y( e6 E8 v" |   while(j<t.length-1)* r7 x& G, z0 Q  M1 J+ R
       {
      W% q5 r  R5 a& M2 J      if(k == -1 || t[j] == t[k])' A2 u- C% _# {  ?3 c
          {
    5 B6 _+ i( H1 k- ^' C         j++;k++;
    4 ^  X7 x" d, e         if(t[j]==t[k])//当两个字符相同时,就跳过; X& @; S' C4 d$ X9 {/ v
                next[j] = next[k];
    . l5 ^& `3 N. S/ R' M         else' C2 `1 y1 l# I- }
                next[j] = k;0 b" Z  q/ Z) l8 m
          }
    , f5 [! w2 h+ X; J; b6 S      else k = next[k];0 ]2 V. d' l- E4 [3 m
       }
    + s' y% S# H$ N! q+ I8 g}6 e0 L# J! C! a+ N
    ————————————————& ?! j! Q* [( v1 K, q
    版权声明:本文为CSDN博主「June·D」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    : D3 N: A  g' i& E5 s8 l/ J原文链接:https://blog.csdn.net/dark_cy/article/details/88698736
    * T/ b- X, Q9 E0 j
    & _  H7 m6 }" E- q3 C3 f* ^+ B3 E- e9 H. w) c( ^, {
    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-7-31 01:02 , Processed in 2.094585 second(s), 59 queries .

    回顶部