QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2954|回复: 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值算法
    4 H. G$ m2 _  l
    9 S/ Q* R3 f0 }. D大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。
      B0 B# X' I% b1 v8 k' X/ I7 ~: @! C8 J7 J8 w$ ]
    0 o2 c1 C) Z. `8 k$ \* ^* O
    本文的逻辑顺序为
    - e5 g, g- f/ e* G( l1、最基本的朴素算法' f$ K+ q& A, i5 g
    2、优化的KMP算法' v2 p! ^$ z) K
    3、应算法需要定义的next值+ M) ~) L3 i5 f
    4、手动写出较短串的next值的方法( k* y3 l2 Q) C1 u( s+ X; m. i8 V+ K
    5、最难理解的、足足有5行的代码的求next值的算法
    ' f/ w' N3 }2 `. o9 m6 ?所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…
    : Q# `& }5 m  x9 W3 t) ~  b, F$ Q$ _& ]0 R2 ]- {; D- x0 ?

    ! C, c, i, [6 s一、问题描述
    ) ^+ j, Y7 h/ F) L* x给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。
      N9 D$ ]% H- Y. Q7 ]; s
    ' X6 c& H% k* A6 S/ w! z

    ) ?% c& s2 O) d; e! u4 F9 p; Z二、朴素算法
    ( X& F' L' P# @最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:4 v; m3 n) y* E, o8 {
    1111.gif
    - K) H: @: F9 m6 p/ U2 L
    ! U8 l0 n! V2 o% R8 z9 y0 Z
    : n; H/ ~" [3 g4 |) `4 c0 v9 y
    " y* K* h; C9 G  j, b4 l5 L5 c
    这个算法简单,不多说,附上代码
    " h5 k1 l% H. p+ b6 T  w
    - J- w  G. A& I! m; f/ g! O

    % {) N6 Y. s9 E4 x#include<stdio.h>  ]1 R4 f. l/ {' _
    int Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数
    ! O: s) g0 J) A+ n+ h! F    if(sLen<pLen)return 0;
    # @  n; t9 Y. J- \$ p0 T( o4 }" [- X    int i = 1,j = 1;) c5 ^+ C' ~' y; h
        while(i<=sLen && j<=pLen){0 i. R8 S5 g- A, u
            if(s==p[j]){i++;j++;}" ]  b7 F% L+ R1 E/ `6 m8 i
            else{
    * s/ [! o+ {5 E6 C- p# _            i = i-j+2;
    4 o! J: l3 C2 I1 u            j = 1;
    & P% @, F! J3 R5 h  s        }) v( `3 |0 J, E7 M2 ]; {% V
        }
    & y" e+ Z2 Y) |+ j' H/ p: {    if(j>pLen) return i-pLen;/ ]3 c5 b' G6 ?/ {; F
        return 0;7 V5 e2 M: U+ G2 W$ x, {
    }* I. W; }0 Y. m( s  I. Q
    void main(){. J) X; p6 l! i/ k8 v+ I
        char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存; ]( S) P# f3 i
        char p[]={' ','a','b','a','a','b','c','a','c'};# u. w) b* P6 ?
        int sLen = sizeof(s)/sizeof(char)-1;
    ) O, \' v! m+ T5 s, c9 ^; z. A# z: {8 O    int pLen = sizeof(p)/sizeof(char)-1;
    $ m; `1 j2 m# D& K& G5 }. J5 X4 U    printf("%d",Index_1(s,sLen,p,pLen));+ |, Z7 \% F( q( N
    }, i( _# i4 x6 i# V" s% G
    1
    1 N7 |+ {8 ?: [+ Y24 @' o% T7 e' c1 x" D# @
    3
    % L- b& H6 h: f- V6 g4" z6 |% v$ {7 i! {
    5
    " t9 }( b# Y' \! f1 F7 e9 H" H60 B; @- @3 F! |
    7
    ! x+ w7 p! i$ c7 g7 n( Q/ c8
    + n" \& n/ i: l  v- {9
    : j1 Q" D, D, ^! ]' l, H. r2 m10
    : _" M$ I5 x4 \: _11
    - O2 ~  {. h& w" g2 a; `+ ^12
    ; H( M$ M- x/ K13
    2 ]9 S8 x6 l6 D6 O5 r" |14
    6 b- w- }! d5 N( w' a1 P2 @157 k, z! `$ l8 s+ ^# N
    16& t  Q: D7 W+ ^/ x1 W
    17
    # k5 [; i/ [7 u% Y2 K18
    5 J$ u1 T1 k$ I' j2 y19
    : H1 d" N8 k, {6 k1 ^* g20
    . j  B8 ]+ r" w+ B0 Y& m/ h( w21" X& D% a2 t  Y  ~" m5 }) k/ v
    三、改进的算法——KMP算法+ ?  U8 h3 y7 l0 D  j  C8 ~  _
    朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。
    2 S$ f* a$ i) U7 M  {一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。- t3 L  [$ q' t7 e+ U( q
    朴素算法中,P的第j位失配,默认的把P串后移一位。6 Z- g+ G  r1 u- m( ~) ]0 o  L
    但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:, C$ r2 L/ A; b  }9 W8 }

      w0 d) i9 Q# R% G
    222.gif
    4 i& X' U. Z4 a
    ) a- _" Z/ w5 _/ h

    . x2 z/ B! A) U- l( F0 ?这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。
    / Q+ X$ C  Q  F+ U: T% S& z' ?
    , K& S7 C* d$ j$ ^4 \; ^; m! k9 N

    ' V- R7 \3 {0 \! Y# U- H相比朴素算法:
    + A$ A8 T; p% n7 f+ D/ l( W朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)& n4 y1 v; V/ s3 F9 f% V  K
    KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高# d! ~4 i  S" M% c

    , P6 f9 \+ @& K) j8 t

    : a3 _# S0 t- Z- K6 I% q! |- x+ N而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)
    7 D0 T% c. Q2 t, [, K. a' D5 D" f5 y% ]- A% F3 `  E
    / c! f: C) ?! Y) T. r" N1 s
    开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)) b8 {1 G$ M' h: `. g: K1 ~" ?
    6 c$ _! V1 T3 z: y

    1 N5 w+ X$ q( v7 b8 _+ i& L比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的
    % G* Y# m$ m- }0 t  A  M
    333.png " ?7 }/ g  @3 w6 d- M. I
    ! d# S2 }4 f' [5 r+ p6 v& I' F

      g' I; |! J* r3 x/ R假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”- p. i, M) D1 p0 f1 T3 o
    ; ^, m& p! o  p5 p: V+ a
    4444.png
    ; N; a( z5 ?. w! a- V9 r  n
    , g5 h- J& v' w- |2 n

    : m3 j% P& v1 O& S# s7 U那么可以推出,P1…Pk-1与Si…Si+j-2
    ! ~% d; b1 r# b- G' P( g# `
    ! W( q) v5 s, k1 x4 z
    555.png 7 Z3 j2 _! w: W9 J9 C
    - p* Y- a9 C" [$ f) T6 J9 m2 o9 y

    7 l8 _: Q4 s, r% P( s8 W) O7 ]显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:' a5 W' f. ~0 c- l$ a* n7 m9 {

    ( A: C; D& @) V. m1 k
    666.png 7 f, F; C& A  a% c7 w* z
    - g2 g! B8 D# m( |9 ?+ h

    : o. @+ v: H3 L  ~8 W为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。; t. _3 J0 Z2 {' v9 b" `5 S$ ^/ a( L

    / h6 z6 u" A9 C: N1 z
      q' i2 ]/ U8 M0 H) W$ Y9 x" }
    最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
    & W3 W. v+ |/ ~- ?5 K1 F+ i$ }: i$ Q( ~6 u

    * M8 }% u- b7 Z& ^/ T1 N6 c四、手动写出一个串的next值
    % A: U$ J5 n" C; `' ~! M我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:2 W+ g# _( t( E# E

    - b1 p; N; ]" W) A
    7777.gif * N. q5 L! D  s' x% y1 T4 v
    这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。
    ! e4 I" V' C7 J# B- o. a+ o# K# F. p; G) Q7 P: Y2 U: v

    7 V% Z, R: B9 [通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:
    * w1 S$ }& Q5 K. y7 _" B: m+ m
    / ]* M$ z5 V+ x

    ! A. ^$ f! U  \5 E
    8888.png ' g6 x- o: @7 ^$ Q4 ?" W% E3 y! U7 e

    * k' k* N) Q" C4 ~5 ]五、求next的算法
    2 X, [1 J8 q, q$ l+ F终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    ( A$ O6 p9 N- G7 [/ s) `- D( M, b& Z' n7 r- ~" n
    ( `, p3 L9 ]0 S. g. L, f. k6 t
    int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度
    ( y2 H' F; F" v& Q, I    next[1] = 0;* h- z9 i) f" Y
        int i = 1,j = 0;
    . k5 |: `8 i1 ]7 [0 G6 N# m0 s    while(i<=cLen){
    0 j' ]' b, \0 [1 g# J        if(j==0||ch==ch[j]) next[++i] = ++j;
    * D5 v" \3 y1 ^, }* _2 n        else j = next[j];
    . W- y* e! q9 U2 k" _8 W    }
    3 d3 v) d; ~3 ]2 b' i}
    1 W0 C: v5 B1 K( f; ?5 O, J# u3 k# D+ X, R2 G$ L1 D
    还是先由一般再推优化:
    0 {5 t- e, l0 ~" C: a& w) j直接求next[j+1](至于为什么是j+1,是为了和下面的对应)
    4 M) T/ m# X$ @) p  O3 U根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。
    * X, i5 P. a6 {; I3 Y不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:" z. O5 k. U7 b6 P0 P. k
    if(P1…Pj-1 == P2…Pj) => next[j+1]=j0 P3 I* ~. K) Q' Z$ W, _/ Y7 f' ^
    else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1( d4 [- B) v* H$ ^0 Q
    else if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2
    " y0 W. e2 x% G: q
    , K/ o. t$ P) N, V! D5 M6 ^) ^3 J: Y

    ! n# c& n' M( h# W  z  F+ r6 oelse if(P1P2 == Pj-1Pj) => next[j+1]=3
    + a; B) O* N/ K' [4 _5 telse if(P1 == Pj-1) => next[j+1]=2
    8 q$ l( k- o' ?* t) e, O- xelse if(P1 != Pj-1) => next[j+1]=1% h7 M0 k1 G) K, U
    每次前去尾1个,后掐头1个,直至得到next[j+1]
    % v5 _+ V' k& y4 p( @  S4 w5 N9 \, Q; n% |9 {
    7 B( Q3 \3 \/ z6 m9 Z
    再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    0 @/ t/ d/ ^7 J1 O但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,
    ) N, @0 e; }3 R# K①next[j+1]的可能的最大值是多少(即从哪开始验证)
    ; I; d) ~  O! A②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)
    : p( r3 q- g3 W( q: W9 x# H1 A看一下的分析:
    * M: ]7 r4 x0 |# D8 C1 D8 ~4 ^- M/ ?: F& y9 V3 H6 w1 f
    % L4 ]7 j( s7 ^# s
    1、next[j+1]的最大值为next[j]+1。
    4 s1 |# P  m  _, c9 a( s7 B  u因为:
    1 `4 y, D2 C- `4 B# Y) I假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。6 r5 \7 H2 n$ i5 K1 L. P
    如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1  ~3 N' ~5 D; o+ A  w
    2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]' m8 O' B3 f. N  t5 R
    这里不好解释,直接看下面的流程分析及图解( r' p2 p! ?4 P6 ^- i0 G9 @
    ( N. i8 u5 U- |( C( Q* n5 W" W3 y
    * s& H  J* U, l+ ?* _
    开——始——划——重——点!
    . Z# J# P/ N$ e" b5 ]从头走一遍流程
    & S  x0 ^/ h; \/ y4 S2 I# I①求next[j+1],设值为m
    * G2 L9 H# m1 l/ o# n②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1( n: c% @4 p5 F6 _' Y1 N
    ③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则! ^0 P, |' T7 q
    ④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1
    " ~  c( Z) F0 g8 I: E. o7 b⑤第二第三步联合得到:
    3 ?" U) B% t7 s( Q# f  hP1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合
      l# Q3 e4 R5 J% ?⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推/ }- y) O4 i. P0 z6 A/ T

    ; F; |$ G, W3 \' {0 R0 v

    % [; x0 @# \* ?+ g5 b- m上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
    # W; z, p3 S7 S9 m; \8 Z% J8 F. P1、要求next[k+1] 其中k+1=17( T8 \3 k3 c9 W! Q7 B
    999.png . H# B7 [9 ]. E* e! g: Z

    - _1 x( J7 z% A3 s& J" a/ G2、已知next[16]=8,则元素有以下关系:: H. q5 Z, U/ r, o
    10.png : _% i" |+ I" a& @/ c* N' z7 {# A

    , S/ y1 V* S; i+ Y3、如果P8=P16,则明显next[17]=8+1=9
    & w* I8 @9 Y" @6 a4 O' M/ v5 {6 f4、如果不相等,又若next[8]=4,则有以下关系' N7 _5 d1 H& o9 e
    + \5 @4 Q5 j2 ?+ t; A1 y
    11.png
    6 T! J$ O7 W, w. x2 K又加上2的条件知
    ( D  p; V( W7 K6 a, M; x7 d% U" |9 x# w, I6 w& T  t2 \/ I
    12.png
    ; p; p& h6 h* _  d+ ^主要是为了证明:, h- P% T8 T+ o! F
    13.png ! t2 h) H- u! c: B- R4 ~* H
    ( [3 o  Z& q' Z3 J- v6 ~( K' G
    5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推6 ?1 Q8 U, g5 W
    6、若next[4]=2,则有以下关系" u4 z0 E8 T( K# x) x
    14.png - J' m4 i3 R4 ~
    ) h4 ~( b$ D" G$ |
    7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!
    & A% [9 y" o$ H9 q0 q————————————————
    0 f' y& g: s3 Z5 C版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    0 U5 E( z& X7 d: s# u+ F* a3 s原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411
    9 F2 {& [! K' I/ q. M, g0 e' }. X1 t7 Z% _; P& o

    2 G) h" p6 r( b4 ~! ]. B

    9 J8 k7 e( y0 P! f1 [. n: T% L) v5 [" k
    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-9-13 19:37 , Processed in 0.429511 second(s), 54 queries .

    回顶部