QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2492|回复: 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值算法/ O1 ?2 C( s0 |( t

    , }' R5 a( `, j7 r大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。& _3 x5 \8 A4 d
    1 V& U- Q- F- G" g7 n, l

    4 ?( |- f- d0 D. P' E/ U) M本文的逻辑顺序为
    . L" U- y1 A* I  L7 n1、最基本的朴素算法
    + g& o8 G9 D4 W6 F2 t2、优化的KMP算法
    . e/ F) F. E5 ~3 e3、应算法需要定义的next值
    ; R2 q  m* I- p2 x6 @' U4、手动写出较短串的next值的方法
    # p; B$ Q! x( |8 p8 @6 K5、最难理解的、足足有5行的代码的求next值的算法) t& b& |6 O) U6 s
    所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…
    / r7 ?# Q8 B) `, E9 ~$ F; Y. p  H
    $ x% A7 v" n+ I" L' @' _* Q  S; b

    9 ^( Q/ `: A' t; a# f) u% z5 `一、问题描述
    ) A- H4 F9 j  O5 |7 o5 G) b给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。7 N+ y3 ~6 I9 `$ [$ A/ @! y3 A
    1 A7 T' G. j* @6 ~0 `; d
    8 A$ R$ c2 O  Z5 b! _( X. e
    二、朴素算法# L3 c7 d% t0 m& u
    最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:1 \2 l7 U6 d3 F- I/ X
    1111.gif 8 i' b# ^) V/ }' u
    " P! f' m  b+ V! N
    9 c( M0 }0 Z+ ?1 t2 N

    ! ~1 y1 \& D2 m这个算法简单,不多说,附上代码' w, o7 u" Z& h3 u& U

    ; ?% _3 n) b5 Y$ Y7 y. S
    5 Y9 K4 Y% \4 C) S/ d
    #include<stdio.h>
    $ x* \" i; r6 r# O! b2 U" Iint Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数+ L5 q. B- S7 _3 J; d; m
        if(sLen<pLen)return 0;3 [+ P0 }7 ~& ]0 b% w+ T( |
        int i = 1,j = 1;3 d+ k6 t' E( k2 a+ p! Y9 h
        while(i<=sLen && j<=pLen){+ R$ Q' [, l! y0 C% S. a( L
            if(s==p[j]){i++;j++;}& O6 N: s  |' r6 O" i
            else{! D9 w" w7 e7 W! a* p* C
                i = i-j+2;& U7 ~) H4 u8 O/ t
                j = 1;( h: H0 E  l/ `, w  T: @7 Y
            }
    ; n# H. R+ H  Z% _" P    }
    & a: a% Z5 s4 L; w% z    if(j>pLen) return i-pLen;* b5 q5 c- G2 s! N9 _, W& k
        return 0;3 ]) @# v8 i' P
    }1 ^; s7 ]: j0 C0 Q& i$ T& U5 B. U) E0 ^
    void main(){/ f" x6 W2 E$ z) J" U
        char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存
    - z( N4 ~' J8 q8 s. V# P: w. a' Y    char p[]={' ','a','b','a','a','b','c','a','c'};
    / M. X: L# \. X' I8 y    int sLen = sizeof(s)/sizeof(char)-1;2 d1 r' V) s, ]! H5 h9 I1 S
        int pLen = sizeof(p)/sizeof(char)-1;2 p0 C, ]9 e' M
        printf("%d",Index_1(s,sLen,p,pLen));5 W; J' N9 X! q! t# C& \# a# p
    }3 u9 @( ^- v. @2 [) p
    1; M$ M* d5 V; W* V
    2
    / n$ ?4 `- x: F6 v( V' {3
    5 V- K  J6 D# X& K5 g- n4. C/ `; ^! G: r) Y8 o' I
    5+ l8 k" u, J8 G
    6, O& H' Y7 J3 a
    7
    : K# U2 z; q4 ^9 I8" t* p3 t" k7 \! C$ X9 X
    9
    ) C* Z* W: P7 @10$ |( o' l6 e# p1 o- y8 k. J
    11
    ; @9 M: |+ Y1 \6 z: Z' c; F5 x12
    3 [7 a$ W' \9 W: B1 k6 x1 Q13
    % {- N3 {- t, ?& N2 F9 Y* H- ^14
    3 J8 Z; @  V9 E$ w; n7 j/ B15
    , V8 V# l$ g- H* R- V! _# s16
    $ f/ P7 k3 h- J. P17( k, n6 o6 {5 h$ V  T% P" T3 P9 V$ J8 r
    18
    3 y1 k2 W- Q2 M' J/ G7 C199 N' ^1 L" C' y3 Y# Y8 @
    20' Q# P5 N! Y- u. E/ `
    21
    . a1 A1 Y; l6 b7 b( L2 m5 R" t三、改进的算法——KMP算法
    : i2 V& J, a- D朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。
    $ u7 _9 R, x6 L, q+ U0 N" Z一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
    ; ^2 Q; ?+ ]0 R. A: g朴素算法中,P的第j位失配,默认的把P串后移一位。
    % h, D0 G4 f: m2 R3 k但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:
    ' X, o  _6 U7 G) q( N/ J
    3 K8 T; c. R3 p5 h% ^4 f
    222.gif
    6 o) S* [$ _3 E2 ~7 }/ o6 g- h# s$ i. G# P* ^3 \+ V

    8 T. i9 x  V- n7 v2 w. U5 J2 v! t, ?这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。5 _* Q  n1 ^7 ^, Q( G% `: M+ Y; F

    1 Q) W8 |$ c4 L2 y% ?2 |+ A8 k

    8 W6 i* A# `, v2 t- w7 r相比朴素算法:
    ; \4 J" _3 s/ _$ f0 ^" a朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)
    ) q2 A$ F8 C' B0 z9 ]KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
    5 p0 O3 G' Q, o- T2 d9 C$ W/ \4 ^1 P' l: }

    $ Z; ~$ v0 N; v- m2 G( }6 ]& t而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。), g/ h& m1 i' U0 a: S
    4 y7 k  ^. ^4 e: l0 x: T  W
      J2 g) J) S$ S/ f) z. `' C% ]
    开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)# j9 y# U+ c, |# Y  x
    * [: G* |# H5 i6 P
    - u0 ^( t  e; W
    比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的
    ( o; |8 h4 R# A* o) w2 x' a& p3 {
    333.png 1 r7 i6 R8 _% }) `8 E9 d
    . C& T5 W5 e, F
    * _( o1 G$ u6 M5 b
    假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”
    0 Q$ `8 g# q1 q" `, i5 H/ b% _7 N5 `3 G; x
    4444.png
    4 ^4 T$ S% s) L0 C7 W
    9 R- W1 |6 d( M! }9 |

    : y. ?% `9 ]) F" g那么可以推出,P1…Pk-1与Si…Si+j-2( [" B  B) K8 L- f+ `4 Y: Y5 d! K

    ! @* O2 C$ \" T$ {
    555.png 6 c% d- Y" [) {( e; S7 S
    4 y, ?  f, |# T/ ]* x9 y

    : Z/ H+ V0 i2 ^# m% G显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:) @6 s2 {9 ^0 g5 U4 o  m! h4 ]& E
    % c6 z  y( j1 E
    666.png % B  J! y; |& {# @/ S
    ' z& Z, c1 t8 ]) Z6 i/ Q! y

    6 ]9 j) v0 X2 H3 b为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。6 i) y8 w# K0 J# Z  w! n5 {
    % r7 K4 g3 Y4 N( q) R% h3 u) `
    8 S+ ^; k8 S+ x9 X
    最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
    % F' o% ?; H4 I4 j0 m& g( p5 t" t) a- i7 [# l

    ' f( y& c& }  R# a- t' c/ J四、手动写出一个串的next值
    # Y, a1 U- V4 s$ K我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:9 _# F6 A; e( D7 z

    + K' e% |) n7 g  R5 x+ y
    7777.gif
    ; `; b8 `5 ^; p' k' Z1 T2 Y; m' S这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。, p) P* S* K! E! M% X
    % ^/ E$ h; d1 T4 k9 H" A
    % h9 ]% G- {1 e" r" z
    通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:
    ! i* u; j! @$ _. ?" B
    ; B9 |) z  j. u! I  c, ?
    0 H* R* i, s- P: @- ?- [
    8888.png 3 [. U6 @& y2 k9 w. X. a( w  K; i
    7 D8 a4 H/ L2 q8 F) n" N, c9 [
    五、求next的算法
    ' _# S9 c' X( i0 Q' z: Y# T+ O终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    ' j7 W/ S6 f/ _& [, w( f
    1 W! X, C) r! C9 f  k' l
    9 s8 }( t& l4 g9 H+ b
    int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度
    ' ?/ U7 u0 B5 q* s1 ?2 Q7 ?" f' L    next[1] = 0;
    + f* o, Q: a( C% o  P) s    int i = 1,j = 0;8 h. f5 r% h" r  m0 a
        while(i<=cLen){" `1 z2 Q$ }8 q) y( c, v- Y6 t
            if(j==0||ch==ch[j]) next[++i] = ++j;4 S0 B# ^( ]( P2 i9 |7 j1 }
            else j = next[j];$ m( P; e& x. G1 }) ^
        }- @; }2 }) r8 N; J. T
    }( P1 a. k1 I' r

    3 O$ [; n: T. Z" h: g  ^& ]还是先由一般再推优化:1 Z* s7 B* n  g9 J: m; w9 k
    直接求next[j+1](至于为什么是j+1,是为了和下面的对应)
    $ w; X8 V! |# W  [* Z; q& O1 v根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。: S  X3 ~8 K/ ~$ }4 C% W) Z
    不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:. S' W. C/ m/ ^; M/ Y: x
    if(P1…Pj-1 == P2…Pj) => next[j+1]=j" C/ {" J  m4 }
    else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1
    2 {% m7 u; @( |% F+ Z- Velse if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2
    4 D8 D$ h& c) e9 [/ `1 `
    ( a& ^4 B: z# Y4 \& Q: ]# h" N& G" d0 N; B# J9 N+ `
    1 m! ~  d7 z4 g- l
    else if(P1P2 == Pj-1Pj) => next[j+1]=39 x* H: m# r3 \+ d5 g* h3 e
    else if(P1 == Pj-1) => next[j+1]=2! w  d. m, h, ?
    else if(P1 != Pj-1) => next[j+1]=1
    0 q. ?3 z3 m7 W. x每次前去尾1个,后掐头1个,直至得到next[j+1]
    0 W+ y% g$ }3 H. g9 ]
    : ^# j5 p; \9 n- s/ e# `0 C$ u

    , i4 A" K# h9 ~% `1 u: u再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    + n' S" K) ^+ t7 D3 `, E+ f但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,' L. F% G; O: d+ F
    ①next[j+1]的可能的最大值是多少(即从哪开始验证), k, g7 T& P( r0 Z8 o  @
    ②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)- N9 \/ t$ w( \+ Q
    看一下的分析:- O5 ?2 m! v2 x# ?( z2 i9 C% x
    ! e) V/ P$ v9 M- K9 z( ~

    - g( U; h! X* w* b& ~( M1、next[j+1]的最大值为next[j]+1。
    2 d/ ~9 ?+ B, q" U因为:
    1 S! K2 x7 [' `. t假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。
    ( o' j1 J7 Z# B5 {如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1% r' m2 t3 j& o% y2 o; x5 n) I
    2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]) ?8 @9 a0 `4 N, R3 r. P) t
    这里不好解释,直接看下面的流程分析及图解
    * y+ u$ K2 v3 B8 e0 r
    4 s6 F4 h9 \! \  u- w

    + k* i5 R- k* n开——始——划——重——点!
    ' I, m7 }# L& J& V: A5 N从头走一遍流程
    ; `" M1 I$ A( E: W4 f3 S5 C①求next[j+1],设值为m
    + c6 }. {7 Z. k4 @②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1
    7 Z. N, Q  I. K1 ?/ d0 q③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则
      _& @1 s: H: u4 N% `* i" O( x" V④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1( X7 l6 Z$ a9 J& U* V& T
    ⑤第二第三步联合得到:- m, V$ L, ?6 J$ E5 d' m
    P1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合3 X9 h9 c  l! j: q
    ⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
    . @% J) {2 f: n) Y: X  Q+ G3 b" G  H
    , H) l- G2 m0 ^5 h7 u9 f3 V* ]
    上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
    4 [( l$ O/ t' g5 ~! r1、要求next[k+1] 其中k+1=177 L9 N- C9 A# A$ A1 F/ t7 \( i
    999.png
    5 Q' R* Q: t% v0 v6 j$ H

    1 a5 X& n8 h8 n' M' j2 W2、已知next[16]=8,则元素有以下关系:$ N& }8 q0 o9 ]  L7 y) f6 s
    10.png
    . `: f/ M1 Z: C4 i
    6 a% N; ]3 y! _- Y0 m/ d
    3、如果P8=P16,则明显next[17]=8+1=9: ?9 v- K* N. ?+ y; H0 O
    4、如果不相等,又若next[8]=4,则有以下关系
    2 A% \/ D8 h9 B; @" i5 B3 w$ F& c9 q1 V
    11.png
    / H! D/ B# W% ?4 Z; U" W: D又加上2的条件知( ^: \8 R# I4 t9 v- a; b- R

    ) X- x: S/ ^, I0 [' {' _) P" m& x  ^
    12.png
    , W6 ?  R9 l. m' r. v; J9 j' o主要是为了证明:" N( x1 ^5 D; ]3 H" A. m; M' O: E, b
    13.png
    * O; r8 M3 b( ^- L1 L8 k: ^

    1 X, b* g! e- P+ N7 V9 x5 U5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推
    8 A8 K) x& Q* o3 b& S6、若next[4]=2,则有以下关系& _: _- C* `  G" L
    14.png
    1 b! g  G# L5 m, V4 g' W! {1 P
    + W; U3 B) q, A! u6 _: k
    7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!0 p" `) s* l: g: Z/ Y. c3 N
    ————————————————
    3 _+ S  V/ C! E) B9 \* \  q版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    2 N0 o4 l5 ^- m' g原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411% K/ h$ W" P5 F" r5 {

    7 K4 y, ^* @' l0 c
    ! E6 s) h, M& g9 M
    * f+ W% J: e8 |( x& L/ k# u5 D
    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, 2025-6-23 15:07 , Processed in 2.155439 second(s), 53 queries .

    回顶部