QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2882|回复: 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值算法6 s9 I6 l9 g6 X" R/ a) m, ]
    & W+ A9 m! a4 j  H
    大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。
    9 @( Y8 f3 K- O" k4 I/ R  c  A- n! j) E
    6 X% i' C9 i7 o0 _6 @+ h, E# o) h, `7 C
    本文的逻辑顺序为
    ) u. i# |' G! m4 T7 f; Q1、最基本的朴素算法
    9 A3 ?! d0 X7 s2、优化的KMP算法, _1 M1 C: N( }/ ]; k! O. x
    3、应算法需要定义的next值" @; r( h. @9 A7 V7 B9 q
    4、手动写出较短串的next值的方法% X. F. S/ P& p. f- q% F  d: s: ^! i
    5、最难理解的、足足有5行的代码的求next值的算法* Z( a# R$ _, V# q( k5 M1 n
    所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…3 |; x' G# n" L  B
    ) L. p7 B* c( \/ _# X
    $ Y3 Y) v$ P0 H  N6 q0 S
    一、问题描述) T- `8 j- ?5 W$ K2 m
    给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。
    + g5 J1 G+ |6 p& N. c4 [& i6 {, E4 a7 W$ L" C' S# B3 C+ _% s( h

    ; a3 ?5 F2 x8 [# S" \二、朴素算法( o' J7 D9 ^/ D! y
    最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:, R5 x3 k5 F& ]( d! a7 g6 S
    1111.gif " f# T' e; p: S; H/ d- z2 \

    8 W+ N' F' w, p# h% X0 v
    0 h3 B% e3 W6 B4 a& I2 z  d% W

    9 O' V8 b* W5 Z5 M" c这个算法简单,不多说,附上代码- H7 y" o8 C" Y6 f; }

    ; b2 |4 I0 e5 O$ r/ X

    1 k) b1 _5 a  q  p. t- y#include<stdio.h>
    - ^: {1 `' M2 r+ p# G8 z  x6 s# n! qint Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数' m0 s8 s0 j3 b2 \- A
        if(sLen<pLen)return 0;, ~0 `6 R3 S1 B) l& W
        int i = 1,j = 1;3 Z! B) x2 _* C9 z
        while(i<=sLen && j<=pLen){7 e# [# K' L  o' f8 `' N
            if(s==p[j]){i++;j++;}" t4 I! F# l- m7 r3 u
            else{
    9 _% {9 Y1 A; e1 @  M; O            i = i-j+2;" b# A+ X; f7 v6 h8 z( G1 C* A- u
                j = 1;
    # C; {3 q5 a( Z; ^2 r. u5 q# h2 x        }& ^- L1 w2 H0 _1 _4 J8 _6 A
        }
    " c; [2 M& @3 I    if(j>pLen) return i-pLen;
    : C3 O) {$ C0 f, }( R/ z) p    return 0;& {2 N/ j- M6 u1 `1 m/ K! O
    }
    & l8 F/ `# p% B% D" f: q5 bvoid main(){9 R  I" h0 H6 b
        char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存( {" a  Y* [  }+ ~( t
        char p[]={' ','a','b','a','a','b','c','a','c'};
    & E( w0 n; }- P! m: \) u) Z+ Y# B    int sLen = sizeof(s)/sizeof(char)-1;, P( {- L! y) y/ E" v
        int pLen = sizeof(p)/sizeof(char)-1;: z3 q1 C. r1 t* ?4 `" _
        printf("%d",Index_1(s,sLen,p,pLen));& T" K5 h1 T  S! ~5 ?4 n; ^
    }4 x/ L) B9 B4 E* k6 \$ b" u0 z
    11 l  \" e/ V& g: m
    2. p# W7 F( M/ L6 J" o
    37 w, F& W- {" k4 W6 Z4 m6 M! w
    4' x. J4 @8 P1 x* I
    5
    5 c# p+ D) T3 V. R& V' A63 D( t4 G8 ?+ Y) u' m- ^& d. _
    7
    0 U% F, R4 R0 z& G- S' y+ d7 {6 d9 f8  i; H- W7 `6 R# }+ S
    9
    $ r$ c; r/ k6 K- J10* m5 W$ ~) v, m
    11
    " A) U2 i. p/ t3 ?2 [$ ~3 B( T127 `; p5 |+ R5 ?0 k9 r9 i# L& L. U
    13, }9 F) L( a( R/ Q
    141 M+ N+ @* I) m: ?
    15% a, L! H3 J4 }- S! i7 l& T  [
    16
    7 \7 Q; O, ~) ^5 s( g+ u# u170 P# D6 {# J. x2 {2 v8 D
    18
    % Q4 h+ u' q( m19
    * J: T  M9 w; \" Y. f9 i) P206 T, n3 {: B3 g% I# I
    21
      g" _0 \8 X+ E  [三、改进的算法——KMP算法
    , X6 Y% {9 z( \, O& W! F朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。+ v( Y  r3 z6 R/ R8 o9 }
    一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
    # m( `1 D5 M4 `9 n8 y朴素算法中,P的第j位失配,默认的把P串后移一位。! \9 R! @1 z: A) K! q9 U* E5 r
    但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:
    ) \* A- Q' p" P' T/ r- k* r
    & [* K6 F( k. a
    222.gif : a2 w1 n* ~: H7 u  ?, I7 F. d# V
    : e% G. C( L$ |& T  D& s
    ) k; ^, p! E8 N6 E
    这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。* s  I* D& ~7 P5 f3 L2 b1 @' \) J" d1 M
    2 M. K0 a. Y# w( Z; _

    ( c1 e0 p: m, J. O相比朴素算法:
    9 _9 n% j( Z1 V1 c  x# E  p朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)
    5 M  s  A( R. OKMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
    ' r0 ]5 Z+ f% T) i' \
    ; S/ V0 L" F) M1 A$ |" T' J
    * \4 y2 w0 q- i: N. Y2 e7 s
    而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)' q  K0 v" ]- Q+ L$ b3 s
    . D( j5 B* J  R

    % v) x% U$ E& y; ?开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)
    / R& W0 z4 G2 ^5 g( o9 }& W
    : K+ X9 _- m7 L) [6 O

    + {; ~5 A# e  [* N, T& C比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的* }- }9 b* u0 [+ ^3 j
    333.png ' V2 y  E8 R# i; O4 ~2 C

    6 R2 f7 w0 Y: M: u. d  j' l
    ) X7 q- z" |4 s9 m
    假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”
    # H. Y9 i  E1 B) R6 x6 _9 C3 a: G; \7 c6 K" w( k) E
    4444.png
    1 E$ h& w# z( A* `) D( p
    2 Z' b) r% \7 m! i
    0 E. i" l) {; v5 ^! ~& `$ P7 w( k* Y
    那么可以推出,P1…Pk-1与Si…Si+j-2' M  y) M+ V, l& T) C2 b
    ( l2 O$ Z: r5 y8 Q
    555.png
    / ^# }' J; m7 P. r! [
    5 G2 f3 C! W7 L( |7 U& ]% Y
    / B- ^0 C; u  K* `" o7 m
    显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:! w" ]  a2 E' x+ `7 X7 l

    * P; m3 h  F* L3 i5 k) {" ?: f* `6 \
    666.png
    # N, C5 E. R' r1 z
    - ]3 D0 r; ]! h* z9 B" a

      F, ?4 q% b$ L# S6 z为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。& j7 M: H! h* p+ t2 L) E/ y
    9 O; n7 s# m; E
    # k2 r7 g. `  o" B
    最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
    - S. |$ H% u$ U5 Y5 x9 f" |- `" u6 M2 _4 \; z

    $ U3 z5 Q2 V! ?4 K0 ^/ a' J四、手动写出一个串的next值
    9 m  m% D# f: `5 b8 t, _我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:- d8 J$ Q3 X; l
    % t0 A, n+ o/ K; v6 l% N$ H! P( R
    7777.gif
    # f2 M! b: S' Z2 [( h) s5 D这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。
    ; g* m0 b9 I1 W8 Y, K/ J6 V* u3 |* o+ L

    : E4 M% `4 f8 q* u; O* a通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:. C/ h+ `$ f+ e
    ' F7 ^0 G0 Z" Q" G

    ; N( Y$ M) k, \1 w' @. O4 h* z
    8888.png
    1 ]# R& h  B1 D7 m6 t- B, D

    % _' A7 F8 j6 v% T五、求next的算法
    $ e. T! b% _8 X7 H5 o终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。0 G4 F$ R# o' U

    % m0 p  U2 v2 S' z5 g$ \
    * I/ S1 n, T! @0 I
    int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度1 F0 ^# D) A* F: ?" s6 m
        next[1] = 0;
    7 E' t0 s" u7 T) d: a7 E7 A( f    int i = 1,j = 0;0 Z. h" V( m# t
        while(i<=cLen){
    : U$ ]" M; w* b3 G        if(j==0||ch==ch[j]) next[++i] = ++j;$ b* ?0 {- D: R% o9 Q$ H# w
            else j = next[j];# A# _/ y  w: U7 _' J$ I: A1 u
        }/ H! d. b* y, c: K2 Q
    }
    ' A& j4 y& C/ j3 A: Q9 a) E( B' c6 Y: f) s' u
    还是先由一般再推优化:: B2 }+ M! f! n( V
    直接求next[j+1](至于为什么是j+1,是为了和下面的对应)
    . @! d3 n6 ]0 o1 Y# V+ I) w根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。8 X% T" j8 t4 t& U' h1 h0 D8 k
    不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:
      n5 P$ g% Z* n5 Nif(P1…Pj-1 == P2…Pj) => next[j+1]=j
    0 @3 P) d* `" W% N! aelse if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1
    2 z! r; H- q- V4 u. @else if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2
    " m; ?7 K/ B4 ]
    / f& K% T# y, t; [2 X7 w: O# q  x& o) Q; q7 ?% u5 D: V

    3 L/ W' }: E# l- u0 Xelse if(P1P2 == Pj-1Pj) => next[j+1]=3# X& P! T( p! k: G; k, N  n" e
    else if(P1 == Pj-1) => next[j+1]=2. Y; h9 w! Z" B
    else if(P1 != Pj-1) => next[j+1]=1
    ; t/ C" I7 m9 s( ~2 i% v每次前去尾1个,后掐头1个,直至得到next[j+1]/ E! [6 _6 x. A8 P$ G1 k

    ( _; M5 U$ O" y3 l) `
    4 l" b9 O9 C5 _+ n: `4 a
    再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    ! Q. p$ O8 t  W$ ~5 |' X% u  C但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,
    % i$ q" y1 C. L' a" [, Y$ g- r①next[j+1]的可能的最大值是多少(即从哪开始验证)
    3 y9 e& Q4 v8 E4 h1 P% T/ U. E②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)
    & v! q! r' k; a7 w' N看一下的分析:
    # E$ U" F. Q% \" Y2 q$ A. ?8 R1 B! U. B
    & P' b- e! V# e; i( T
    1、next[j+1]的最大值为next[j]+1。
    3 W  |, g/ ?, m/ C/ y! x. D因为:( ]0 Y# Q/ s; Q- p- W
    假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。
    2 q6 T  X9 ^+ p. g" J) M& v5 U如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1
    : q6 L# W; k8 ~0 L# i2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]
    / }/ g/ L+ s! g' I* Y/ t这里不好解释,直接看下面的流程分析及图解, c9 r; m' i4 T+ r+ ]- E0 l
    . }2 b& m( @2 \6 g' S1 m( e% Y. L

    7 y: t: C: R: w开——始——划——重——点!* q: j# J. H% C- G- y1 J1 u
    从头走一遍流程
    8 Z% s/ p4 J, u# L2 N①求next[j+1],设值为m. ~. T" M7 u# _3 I* u  n' r' m4 z( j
    ②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1; Z' `1 U4 z# a. l! B, @
    ③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则
    / b0 m+ d( v$ D④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1
    " S  N) ]+ J& I8 p. `( t, |& J⑤第二第三步联合得到:
    5 ^' @8 n/ n6 o: q/ ]P1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合
    3 A1 I( A) a6 K2 T4 q⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
    8 E! ?4 F. P6 N
    $ N0 T* Q) H7 S3 v7 `, p

    ' U4 i* I/ J' R/ g% B6 D. q' ]& R# n) I上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
    ; x% D. A. U( M7 H1 l% q$ o1 J1、要求next[k+1] 其中k+1=17
    " C7 I3 S  z, C9 V) `
    999.png
    ! E4 S. v# {6 ^0 s  O6 K
    / `* z" c9 I/ R# `  i* V" S4 [
    2、已知next[16]=8,则元素有以下关系:
    8 Q6 ^9 N5 _. ?3 [4 t
    10.png 9 d4 {! p2 X# Y* k  o. W
    8 r/ ~/ X. P: M5 Y/ D
    3、如果P8=P16,则明显next[17]=8+1=9' G$ W3 b$ x* t* J
    4、如果不相等,又若next[8]=4,则有以下关系
    ! Z9 {" d! }; O4 s' S, _
    : j: ]2 h* X/ b/ f" c* I" e
    11.png % ]) c+ p9 O/ l5 {" S
    又加上2的条件知7 `- d$ I1 C! P) v6 A( a

    : ?# f: K. n/ t4 {! U: \8 H
    12.png   ?5 y9 b: L, I% n, q+ O, a
    主要是为了证明:
    % }, Y/ \  z6 J5 @
    13.png
    + O. ]( M; _% H; r. E' [  ]: y# h
    2 I2 r: K0 \6 B) Z1 b6 H5 ?
    5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推+ b$ I: F0 n& |% L* u4 G
    6、若next[4]=2,则有以下关系) g+ B+ Q8 |0 f  Y& ^8 c# l
    14.png
    # W2 |/ x# P( ~
    7 u" C0 {6 p) m3 [. y4 X
    7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!
    # E1 A% j7 v; F+ W' s& Q: V5 A————————————————
    - W5 G, K" f/ J4 {# U版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。8 ]" n( T5 w: e4 B1 f0 h) a8 C
    原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411$ K! k: c$ i- t8 Q$ F/ a

    4 r5 j9 s2 g: ^+ |' J- m
    % ^, U% a% d! ]' J2 D' u1 C% s
    ! B5 ~: Y% O0 `8 s
    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-18 10:37 , Processed in 0.450856 second(s), 54 queries .

    回顶部