QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2936|回复: 0
打印 上一主题 下一主题

(算法)通俗易懂的字符串匹配KMP算法及求next值算法

[复制链接]
字体大小: 正常 放大
杨利霞        

5273

主题

82

听众

17万

积分

  • TA的每日心情
    开心
    2021-8-11 17:59
  • 签到天数: 17 天

    [LV.4]偶尔看看III

    网络挑战赛参赛者

    网络挑战赛参赛者

    自我介绍
    本人女,毕业于内蒙古科技大学,担任文职专业,毕业专业英语。

    群组2018美赛大象算法课程

    群组2018美赛护航培训课程

    群组2019年 数学中国站长建

    群组2019年数据分析师课程

    群组2018年大象老师国赛优

    跳转到指定楼层
    1#
    发表于 2021-8-10 16:12 |只看该作者 |正序浏览
    |招呼Ta 关注Ta
    (算法)通俗易懂的字符串匹配KMP算法及求next值算法
    3 \" Y  e& a( S* l$ [# `" I: z) r& |
    大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。
    0 F6 ?+ |8 U# A7 @: C9 z4 |8 i
    + b& E3 @$ g) }2 e0 n. E( Q

    + S; K6 f* n, c9 y本文的逻辑顺序为
    ! S9 T, m+ W% I1、最基本的朴素算法
    0 W$ n/ s( P) C8 W  w1 g5 \2、优化的KMP算法
    6 e" F' ~% v1 x3、应算法需要定义的next值
    0 ]( j9 k: a; r6 Q5 e7 Z- a  y' H4、手动写出较短串的next值的方法' T+ R7 ]: a3 ]2 w( F  _
    5、最难理解的、足足有5行的代码的求next值的算法# L0 D9 Z/ I6 u, i7 s
    所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…$ K: t" l* ]( W
    1 L: y4 H# K1 ~4 r
    * j2 Y) a7 m! m1 J% e% y! o
    一、问题描述0 I, z2 a$ [7 j, ^8 n/ }9 d: x( |6 u* k
    给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。$ E( C! I% s: b, L' m/ V3 B3 v

    / S3 X( r+ F# G5 m$ w7 L. m$ Y
    . g  ~: S% o" f
    二、朴素算法
    / M2 o# A- L$ z  u) Z3 Y0 B最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:$ N! ^5 S% ?2 V% v' f( }# {
    1111.gif
    6 i7 m- T4 j$ L
    " [* X* a4 J) S2 x; b: C  L! P* t! X

    2 n! f8 [& H- h3 G6 |$ t8 M

    5 d; t; B- ]& |# {2 x3 `  {这个算法简单,不多说,附上代码+ c1 T, V, n" ^! {1 O( n  ~! y
    , u) r% ?- ^/ L
    7 A, P& [3 i0 d0 l# Q
    #include<stdio.h>  J, S1 U! j$ L7 c
    int Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数: y% ]: D$ w! `: P
        if(sLen<pLen)return 0;
    , a2 V, ?" e7 W  a    int i = 1,j = 1;) n, `& ]0 @; r. c4 f
        while(i<=sLen && j<=pLen){
    , Y5 N) Q+ Z, [        if(s==p[j]){i++;j++;}
    6 n4 n9 i3 U) |  H' Y# d5 S        else{4 ]7 e/ y3 o6 Y+ j# A
                i = i-j+2;( J0 n* G, ?, H8 s* ~
                j = 1;
    6 L* u: c! y6 x5 F1 _. v6 v1 b        }
    ; b9 @& @, |+ Z: e) F; a# M    }
    ; u0 d+ e- H  y9 G    if(j>pLen) return i-pLen;  G0 b; f! G4 p+ a( J! E
        return 0;; X& L6 q2 ^  b: ^
    }( \$ {" k* Y4 a, h
    void main(){
    " ^5 s( S  h. m$ O. h    char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存
    0 O. S- u! Y7 v4 o' H9 P% q    char p[]={' ','a','b','a','a','b','c','a','c'};! j' [. @( X- k, b
        int sLen = sizeof(s)/sizeof(char)-1;
    4 ^& V" v  ~% {, S8 p; x% P    int pLen = sizeof(p)/sizeof(char)-1;7 V4 L$ _7 _! Y$ o
        printf("%d",Index_1(s,sLen,p,pLen));
    ) r# F3 \8 N0 H}
    ' K2 n( B; X2 Y; p3 {! Y7 F2 j* I# E' v1
    $ M+ }- N$ b6 ~& i2# o( T+ s+ O% x- A$ x  Y
    3
    , ^' q  b! ~6 X9 F5 Q49 j) E' m4 t( h$ ^5 @1 L7 a
    5
    1 m9 @- t1 N; \2 M8 M  e" @6 b" s8 x$ m61 _9 w, n; d: `
    7
    / q; I3 F7 w  r; I7 z3 [5 C8% ]% h* z# j! W% b
    9
    9 p3 x, e" X8 O+ W10; G5 m$ B# F8 I/ W4 i( x! x0 [3 r
    11
    , b8 r: c# t: F3 }2 n5 X12" ^8 s/ z  x( D$ I2 Z- `0 k0 G
    13- u; e7 X  o0 [5 h! T$ g
    14
    + q% D: h+ k2 o15- M  N+ e! P: J$ g
    16$ z; M/ k# |6 Z9 R. A
    17
    1 w1 m$ O  U8 f  n2 g6 f$ d18
    : `# K& F9 k5 D7 V2 G19
    - Y: ]* e+ x  Q20. H/ \2 S0 u  e6 k5 L
    218 {4 a6 c: Q  Y& W& b+ j: Y
    三、改进的算法——KMP算法6 R4 U+ `( O' i4 t/ q0 m- \
    朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。7 E: E/ u$ Q! m6 @
    一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
    4 X3 N8 Z7 j6 H4 d6 n- G  Y# L/ @) k朴素算法中,P的第j位失配,默认的把P串后移一位。" G4 u1 q. c) j* g, [
    但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:
    % |! S4 L8 i; o0 w- @5 s* r* Q* D* M, K4 x- P( o
    222.gif ' M/ L; r' k" F/ {+ ^( |  G

      b* Y- U# E5 W+ M

    $ j* C* O* ]- W" I9 b$ R- U0 G7 |这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。
    , c' r2 t) |1 [/ d% i5 f9 @' }+ ^/ D- m1 U! f

    $ _/ A/ V5 a: N) I- e! B相比朴素算法:* @# ]( M# B/ D
    朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)
    8 z; b# s, [7 u* V9 ]KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
    - p$ C' c, v% l$ X! R% f; W$ L, P  r
    0 [" ^0 y% e) f# c
    而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)6 _' z) T8 ]9 ^8 @5 R4 V

    # n! L! f0 `" v1 S' k/ ?! x
    1 B  j% k6 @8 T$ r  y, [( \
    开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)
    ! k) D+ Q8 g4 q' M3 O6 r) u4 E+ D  F
    ( |6 g/ h$ m/ s  f* t4 g3 D! U
    比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的, ^6 [" H# u7 v! n1 c6 q# g
    333.png
    , W( d9 S  u# t# F7 H1 k0 r$ X& E/ \
    , r, S  [4 j" h

    . T# }$ e4 ?2 C0 g) [% ]假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”
    2 ]; x/ Y# B: X% h  s- m& D- I0 Y2 s0 d1 x% ~6 W
    4444.png
    + q4 {& h9 `) M8 A: s
    . ]8 k, W$ C1 o1 _* P

    + O- a/ j1 b; j那么可以推出,P1…Pk-1与Si…Si+j-2
    ; L! U% I/ e# B$ z3 U
    8 z+ ~! e  Y, n! l
    555.png : o6 s  X/ e  Y" P" d6 r
    3 F; l- f5 R8 P( A+ @7 J- O
    2 ^+ [; z$ T; w1 ?# H) k
    显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:
    4 _, D2 K, P# Y0 D& D: i2 o; S! K& S4 Z2 W
    666.png 5 Z# s* n, M# E2 x* r" L, ]
    % t5 ^  Z0 V0 D" K$ }
    / J! c4 f9 L. G9 Q( g8 l+ I
    为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。6 Y4 E$ Y% L4 X( s; V+ b
    9 o- J% Z+ R1 D# V0 u
    ) o# w  r2 U. J, N; |7 _4 G
    最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
    & @% u, E& R% G+ a0 ]# |" S& @( t% G8 a% ]! a' ?- H$ w' e

    * E% f: E0 o! }, L) ]: @四、手动写出一个串的next值/ I" U  f' F( m- R+ K' o4 I
    我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:& \9 H/ R; e! p* p$ {6 e/ g
    8 C+ i5 `* a; Q, r+ R0 h
    7777.gif
    : t' ?: }, C* q5 e* o这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。6 D5 K6 m, \/ P

    , M5 l5 X- W0 r9 C4 s  v

    7 e* C5 {2 O: S( F通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:
    + n0 D, j2 f8 A3 ^  ^' x- e( q' c6 ^' {2 z/ Z; U* l

    % E2 v0 D. M, j4 K3 C( a7 O0 x: J
    8888.png
    * D) w6 w' X7 x4 f. _
    0 Q& L. D1 f0 V! ~6 |2 P+ f
    五、求next的算法
    , ~! n( X& E& D" {8 e$ Z3 P终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    : e$ r2 l3 y+ c* D2 M+ |5 k' ]& a+ j) J  z$ G
    + x& c# b$ d- S9 b
    int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度' m% g+ Q( K# ~( p
        next[1] = 0;
      }* @2 E/ e2 K5 p, r0 z    int i = 1,j = 0;% k9 j' S. ^3 Z
        while(i<=cLen){
    5 J0 z8 t* \9 B+ e, B% R        if(j==0||ch==ch[j]) next[++i] = ++j;
    3 k' _! s% F6 c: q4 l2 [+ s* i* q        else j = next[j];
    5 `# f4 V. i. D" r0 y9 Y    }; O, s9 ]! F# s4 q# b
    }- I8 ^1 d# H- x. R1 n4 X
    ! x7 d+ Q* R( _6 s9 |# F- `
    还是先由一般再推优化:1 E/ o3 Q. o+ ?( m4 e! `
    直接求next[j+1](至于为什么是j+1,是为了和下面的对应), Y5 u% U7 j8 a, A# [# d& d
    根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。  x9 I5 S. W# A' V; F$ h# l$ X) B
    不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:" d0 H; c. f9 q  y+ p# `6 M
    if(P1…Pj-1 == P2…Pj) => next[j+1]=j, ]& y4 ]6 \  T4 Q, [+ y* s, v
    else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1: u# Y' K. ~$ k
    else if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2, F6 J( ~- o$ s  Q: D/ _0 p: M% j/ e
    ' _8 e# R) Y- _

    . N3 ?+ l; F2 Q2 D5 T
    ' h+ W! D3 e- z3 ?else if(P1P2 == Pj-1Pj) => next[j+1]=39 S+ G$ T1 b5 m3 b9 j
    else if(P1 == Pj-1) => next[j+1]=2) k8 B) W* }  k( b
    else if(P1 != Pj-1) => next[j+1]=1. O9 @$ G1 I; ~8 u7 @
    每次前去尾1个,后掐头1个,直至得到next[j+1]
    + f1 L5 t# Q1 W" Y: P1 A4 t
    $ |* S! K+ N. t6 _
    : o% J  z3 C2 p- Y) V# O' Q. y
    再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    $ [, Z. c% S  c8 ]6 y4 L但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,
      T3 ]: E1 M) ]7 D) K6 i①next[j+1]的可能的最大值是多少(即从哪开始验证)6 z" \% h3 b; Z+ R
    ②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)
    ' |1 h9 R$ H5 T看一下的分析:% x( g. z1 [- c5 u1 E0 T
    7 A2 W* ]$ g7 W, ^5 v  i0 J

    % a$ P) T' V5 R, w0 o# Z" A1、next[j+1]的最大值为next[j]+1。
    # T  [% D3 N8 i因为:
    # G$ _4 d" U* p; p$ b假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。
    : g6 B$ `1 c9 [8 g* h如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1. @6 ^) N! m/ G  C: s( b
    2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]  P- a5 ^! A5 _5 l
    这里不好解释,直接看下面的流程分析及图解
    # U' u% z* d) v8 U* Y) d- S6 P( ^3 a
    ( J& \* J- k' ^7 ?  Q) V
    开——始——划——重——点!/ [: `; h6 {3 J
    从头走一遍流程! u+ U5 j7 k7 H& T- S+ S# [/ M
    ①求next[j+1],设值为m! f; c1 h, s3 X3 \! V$ W
    ②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-19 e; f7 @; k3 v  V
    ③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则
    % l* c0 e0 i6 ?& y+ b④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1: h& o/ C0 p- ?3 I3 d6 ~4 l
    ⑤第二第三步联合得到:
    # x8 O/ x" e# r% Y6 hP1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合
    . j0 x9 _7 Q$ M% ?) Y5 H⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
    . ?' w; ?+ X2 F! A+ X' v; S3 \/ I8 k' j8 \$ ]9 G
    ) }# O+ s6 n! z3 Q( a
    上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
    2 {# p- ?) k7 \. ^1、要求next[k+1] 其中k+1=17: V  Y7 o5 \6 W0 w9 A0 E
    999.png
    ! V2 ]' I6 o! [5 S8 ]
    5 v" t8 x% {2 A1 D% G- u
    2、已知next[16]=8,则元素有以下关系:
    & n: j0 [& O' s  r3 `8 m
    10.png , ?$ Z7 j4 n& u

    ! |% _/ I$ S! A3、如果P8=P16,则明显next[17]=8+1=9
    4 y$ j9 `! r" O$ G' ^4、如果不相等,又若next[8]=4,则有以下关系
    # C- M" @  z. {+ U" J' o7 i1 J1 v2 j6 R+ ~3 {, }: _" ]. ~9 j
    11.png 4 ^& Z# O% B( H- s
    又加上2的条件知
      A& s" e7 L3 D% K
    8 O& E. h+ `; p2 Q' ^
    12.png ) S* P) o: V. o0 Y! o' z, Z
    主要是为了证明:
    . R; D) p7 q4 M& P: p, {
    13.png
    8 g* x+ u8 V2 K

    / s  I( ~8 X- b7 _+ @5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推# T9 f* E+ y- b0 ]6 v6 H% c1 f+ I
    6、若next[4]=2,则有以下关系2 F1 d& C, ^* @! x
    14.png 9 b. I0 z7 z; a& m
    ) I8 \1 s3 h1 y+ w& |( I: b
    7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!
    7 f# r, V! d: \3 [4 Y————————————————! ^3 b; k* a5 h& _) j
    版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    4 H+ s# I; W1 @, J4 D0 d原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411
    $ p; V& }; r' C) G( p! m$ L
    $ _8 K2 b; b, i; O1 b0 p( ?( V7 ~, G2 P: i; {0 Z" K

    % t9 n% o' H- [2 t
    zan
    转播转播0 分享淘帖0 分享分享0 收藏收藏0 支持支持0 反对反对0 微信微信
    您需要登录后才可以回帖 登录 | 注册地址

    qq
    收缩
    • 电话咨询

    • 04714969085
    fastpost

    关于我们| 联系我们| 诚征英才| 对外合作| 产品服务| QQ

    手机版|Archiver| |繁體中文 手机客户端  

    蒙公网安备 15010502000194号

    Powered by Discuz! X2.5   © 2001-2013 数学建模网-数学中国 ( 蒙ICP备14002410号-3 蒙BBS备-0002号 )     论坛法律顾问:王兆丰

    GMT+8, 2026-7-30 09:40 , Processed in 1.045375 second(s), 54 queries .

    回顶部