QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2872|回复: 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值算法$ }" [1 N! y' {; f0 B0 ]( ]1 y

    1 ]& P7 }, k0 y: k大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。  n0 S+ S# _+ [! l/ b) b
    0 a0 _- k# J5 G

    " v3 p: `( }# c/ I9 |6 E本文的逻辑顺序为
    ) B+ c$ p8 U  h' c$ r- B1、最基本的朴素算法1 D0 q7 r1 G( l
    2、优化的KMP算法$ }2 X( U$ b" X0 d" g* [* }
    3、应算法需要定义的next值2 k2 h9 A6 q( A5 E
    4、手动写出较短串的next值的方法, A8 e6 [: C1 L; S
    5、最难理解的、足足有5行的代码的求next值的算法! o; I  v: M4 X, E
    所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…
    8 `7 v$ }% d2 c5 X5 h: t3 |% B1 Y7 `* V1 y) [0 B" N
    " T# H' o4 t: D4 s4 D
    一、问题描述- W& ~" G- f4 g6 b" \2 @  q
    给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。
    7 v  i( ~  X5 R' T; u" e+ X7 \; g  ~
    1 h9 H  a3 z7 m' d1 l& [0 ^! F/ H6 t
    二、朴素算法! \0 ]# [9 [6 C- F5 ~" n
    最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:; a! a$ y; p" T
    1111.gif
    7 `1 X/ Q, @* C) W) U
    " P. \/ F; t( x7 i" F; j
    7 R7 j& ?1 a0 _
    ' r9 T/ g- C1 v7 v/ o* q
    这个算法简单,不多说,附上代码2 Y3 w# B# ?: y

    7 \$ A' L3 R. R# k

    ( ]9 Q. F' K6 t" K" I#include<stdio.h>
    , }( k7 J5 k* j' m- z) G1 sint Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数
    $ S" F; \# {6 d( T8 j7 b0 Z    if(sLen<pLen)return 0;
    3 A( ?+ q* f. [9 ]. R6 r    int i = 1,j = 1;
    & u' b8 p7 e7 _% F9 E2 M/ [- m    while(i<=sLen && j<=pLen){
    , V: q& ^- R# Y7 y7 e6 B        if(s==p[j]){i++;j++;}# R' ]! L% Z' }' A+ Z  b
            else{/ X) d) N$ ^: J6 [- N
                i = i-j+2;
    . ~5 z" Q. b% S4 @' i, M7 z) L- e            j = 1;
    - u+ u" i$ J( o6 D+ `- `        }
    ) `- R- G7 u" F" s# s, J: J, O    }9 v) _$ l: G* n1 Y, [
        if(j>pLen) return i-pLen;
    % U5 f& N/ G8 A; u9 N( V0 K" c: m    return 0;
    0 K  i  P& |* i; b" f" t$ P7 W}
    ; o- I3 j# f1 T0 F+ w7 fvoid main(){
    4 D7 {6 z& I, e+ M* }$ d' b    char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存' p2 [# r- X1 I7 {) z- {% d6 b$ @
        char p[]={' ','a','b','a','a','b','c','a','c'};
    + j7 `" c/ G( c& a7 L    int sLen = sizeof(s)/sizeof(char)-1;6 o1 C' b0 r, a6 t1 A2 O! p. V3 O
        int pLen = sizeof(p)/sizeof(char)-1;" n* p. K5 x2 }
        printf("%d",Index_1(s,sLen,p,pLen));3 R, P5 H5 D5 J: f
    }
    ; q# k! L/ q$ N1
    ! ~$ @4 m0 `, B2: M9 W, H: F- f( k0 a
    3
    % L/ n& ^7 ^" ^% p4+ I: K. ~* [+ c' c4 T0 _) J2 i
    5
    ! I2 W8 Z8 T9 M3 M/ N) _4 l6: \2 r- f& x0 ^
    7
      n& z$ n; w  \" X% ]5 B+ B+ h, Z8
    . @" W: z8 _5 B9( {6 |7 t3 e! Y: O# |; K$ T  c
    10- c# j) j. ]9 ]3 e3 E9 f. }% l" J& H
    11
    . {2 M$ n9 i' N0 R12
    + P- b& c' f4 u- M, f' _% M4 o/ F13
    ! B2 o( u; u) l, X. U# ^. z14# x) c7 Z$ Z# m
    15
    + l, ?& n2 J  D) H1 y4 l+ b167 m8 ?. p1 R7 T! E
    17
    , v' u% L) V2 d* z8 ?18! X' i4 `1 ?( [  a
    19
    ' R/ h8 ~5 i0 H( `4 L20
    $ f1 N  {4 m5 ?; s21' T# ^8 @% Q* P0 u
    三、改进的算法——KMP算法7 k7 b/ h  u1 M9 K( P+ ~! k
    朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。. ?9 ?) w( u  g* Y
    一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
    2 D& c# B1 R: w" A& e* u; _朴素算法中,P的第j位失配,默认的把P串后移一位。
    8 \% n) B5 \6 ^  a5 g2 `5 G但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:; h- K4 _7 X0 I1 j2 s

    6 o# F; S( g$ @4 P/ D2 J1 ]
    222.gif 8 I# n6 C- w, d# x2 v8 B

    2 \) o9 o+ G" V! T
    4 N% l5 d6 \/ i% ]) ^* \7 x/ j
    这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。
    , p/ o' N+ M* T1 u6 d( x4 M. v) D; C9 Y1 O
    5 i1 j) r& p, s& O' g
    相比朴素算法:/ E9 r* y% l4 \. t
    朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)& q! `8 w5 z5 d' E
    KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
    % @8 Z( D9 s9 r4 V# r# w- G+ m! X5 i

    6 ~' Z# _- j5 w& c+ N而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)
    * I- z$ {/ u% v- G) t, j* J
    $ c% b9 D5 p0 o$ H2 v  v) w

    : l4 k& S4 j% P( D& w开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)
    9 F/ O. |' ]4 {: o; M2 S+ ~
    1 a* s8 v. R" N- X, I+ P! P- i

    - |3 F0 s" G+ ^; D; S9 t) a) F" }比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的9 I) A3 E/ V, Q7 b/ m/ J. b
    333.png / y4 l: |7 R. W, R7 T6 x; `

    * u3 x: j# \9 F
    5 ]% ~' |) T* L' f
    假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”  @# W4 ]5 i  W4 c
    $ i3 y. }# T8 j& z" p1 d
    4444.png
    6 y7 Q$ d: i5 x) @5 W9 M3 i' f- E' B1 N0 G

    ! I1 w; c' ?7 p# D% O那么可以推出,P1…Pk-1与Si…Si+j-2
    . ?+ p: c, K  R' @% W, {. C( v- X
    555.png
      M: ?* }1 e- X8 V: b
    5 q  O, Z' w. l& Q# h

    " W$ B8 m3 g% s  u% h! E显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:- o6 t: Y# Y7 E! C9 P' X0 \6 q
    # M  l3 U5 |: Y4 u7 J  c, F1 Q
    666.png 7 H- q' k: b9 _! j8 U

    4 l5 j; G8 J% W

    6 Z( _) T4 L6 H为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。  M) ]$ H8 N, \" J5 W

    * Y- b/ i' ]; q+ S* `+ H5 w' r

    . @% r( A: r  k最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。7 z* B$ S) n- k8 ~- P5 s
    ' b1 \# ]3 u) L: D
    9 m5 u8 w& ~4 [
    四、手动写出一个串的next值$ q! t* C! L1 e7 f8 g3 f1 b
    我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:
    % c( n; n6 |& m2 N. }% g3 b+ X
    7777.gif
    - ~0 u, l- n& \! N这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。* ?( d$ ~1 N) o3 f( M) [
    9 V" l  n" j3 I( f. l6 ~
    ( B$ ~' L! _; Y6 [0 S
    通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:/ |9 l" k' M* _

    0 L, }, _0 Z- P' Y3 Z
    + G- L% j# H- t4 M
    8888.png 8 n, j" d  O1 V& z% x1 x

    ; o& Z" \' |( y' p' X, D, {五、求next的算法8 i( U- x; s! S; d
    终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    # q4 X: f$ D: r2 b& l: w4 l, v2 r& E  {0 v' s( }
    & v% D! K  \2 @7 H* Q- U7 K* _
    int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度
    % ~% V9 R8 L4 b, s( Y% D    next[1] = 0;
    + w: H, J  w# B) h/ I  r) e    int i = 1,j = 0;! [& q, p/ {& p& `
        while(i<=cLen){' K( N! o- c2 i" o0 J; F, D
            if(j==0||ch==ch[j]) next[++i] = ++j;
    + u# }3 v+ @& B) Y        else j = next[j];) K0 y/ \- t! t! p4 Z( `
        }9 s6 }" s+ |" T0 D  V/ p/ x( d
    }
    9 I; M) L. R0 b, o: P5 b% Z; R# F2 ?/ v: n% Q5 h
    还是先由一般再推优化:
    7 r+ w: f7 M4 Q2 S8 C  w+ [1 C直接求next[j+1](至于为什么是j+1,是为了和下面的对应)
    - h5 v7 Q0 v, R根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。/ R& j" b2 D# O' z' y
    不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:- Z: i- Q2 r0 E. P. J2 s4 L
    if(P1…Pj-1 == P2…Pj) => next[j+1]=j% T3 H& F4 D$ y, t+ ^' {/ ?; G
    else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1
    ! ^8 ]2 f; A7 A8 \3 Ielse if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2: A2 _6 Y# {6 y4 k4 `# R
    / }: ?' `2 ]! @% d, P# _, ~5 E

    + T! L- f- D$ v( E3 C$ ^8 Z6 n1 }* q. `% n
    else if(P1P2 == Pj-1Pj) => next[j+1]=3
    $ h; y& j4 G. y, Yelse if(P1 == Pj-1) => next[j+1]=2
    , `2 e- Y! j# `- H1 Z7 Lelse if(P1 != Pj-1) => next[j+1]=1
    5 {+ o" d4 o4 _5 A/ [" H( h每次前去尾1个,后掐头1个,直至得到next[j+1]
    # `. p9 Q6 \1 d5 T1 s) \
    4 c. h/ G6 a* B8 j+ _& M
    4 D9 h  _: [) A; w# r) {
    再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    $ b# O. i$ Q8 b, u但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,1 p* A3 R3 {& p5 y4 _
    ①next[j+1]的可能的最大值是多少(即从哪开始验证)
    ; a1 z% c0 G5 X②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)
    5 p2 |/ p; n; o8 W看一下的分析:6 d+ ]' `- Y* |

    , y! x, v, b9 U9 W6 h3 a" @

    2 F  g: z, H3 T" ^  l1、next[j+1]的最大值为next[j]+1。& S8 [1 G" N9 b' q# ]% l
    因为:
    % b5 T! t+ G6 F/ ~; }假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。* s' k6 W; d5 |
    如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1% W( M4 x: Z- y  X' N
    2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]
    0 H( o) ~! m* a# ]% X这里不好解释,直接看下面的流程分析及图解( j5 [& f+ P4 k& m* P9 y" n, j" u
    + K( N4 x% G; }  E9 _% w; D

    - d9 R6 U1 {! l2 d开——始——划——重——点!
    : J) I: T0 G& k: c0 i- M4 O从头走一遍流程( L0 ~" j" A' a0 q2 {' f/ [
    ①求next[j+1],设值为m
    4 O2 `& R! ?9 M# |②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1
    % W% w1 t1 r  M( H③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则2 D' d2 C( I) M0 j: l
    ④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1  _' F1 t# ?8 I& e" S  ^- M* n
    ⑤第二第三步联合得到:
    2 [) O4 d2 n( {P1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合
    4 Q0 K7 t1 ^  J) b7 _4 R; c⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推. i6 l8 G5 X& Z; T' C
    7 F1 Q+ [5 x% G2 x

    8 ?4 H* A) E* m7 k上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:0 b- b0 P, m; Z5 O. @
    1、要求next[k+1] 其中k+1=17
    4 E, L# w% n3 {" n+ t; \4 T
    999.png ' {' n4 M1 i  U0 x% w
    - j" C6 P6 f8 j" B
    2、已知next[16]=8,则元素有以下关系:& J2 j2 C2 b9 L. x8 F# t
    10.png
    ' t. v2 n7 [* l2 ~! y4 h! f

    + k8 c9 M( a% a3 B7 r3、如果P8=P16,则明显next[17]=8+1=9
    0 h, A  W2 @& W/ ^4、如果不相等,又若next[8]=4,则有以下关系
    0 Y7 k/ }9 l7 |  T
    ( y+ g# I& B& X5 W% r6 {' N
    11.png ( E' i" g7 W/ M* N$ x1 N" f
    又加上2的条件知
    $ x- k8 I+ |7 U9 }" L( R, \: z; B; j2 _: Y* C7 A/ {
    12.png
    3 Y6 a+ o0 I% S* Q" a, |+ X9 K4 Y主要是为了证明:
    * R; j/ C5 z) \! [# Q1 A" R
    13.png
    % Y7 l  n2 F' P0 e- y% b$ |0 q: W
    + e/ U+ t! t5 f+ w! r8 N& s
    5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推
    # U+ _) u3 Y5 F1 r* A6、若next[4]=2,则有以下关系' |* S: A5 y; O
    14.png
    ! k7 ]- I: P# Y; F/ z1 x3 q6 V, L' e

    / s5 U! ^7 T. g5 D/ s% ^8 t4 v7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!: Z: |' }4 g  S9 s" F
    ————————————————1 ]0 \$ X' O/ ?% J; v. {* N6 m
    版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    9 P' w0 H! M& _& @- K原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411
    ) H) ?3 s0 C8 o+ {! j$ {* g  f7 u9 I9 u; X& a
    ) Z8 S7 E; u5 S6 H: R: \0 |5 G

    + p2 ?$ q3 v% m
    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-4-10 15:05 , Processed in 0.468975 second(s), 54 queries .

    回顶部