QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2937|回复: 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 r$ o1 s6 j! a2 |2 H. X9 X* T* N) n; J3 N, z
    大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。
    # o; l; S6 s& ]* x. _- [4 d6 j8 M; S. H. L; S8 t# K" o
    4 x9 @; p9 l  A: C' W( p
    本文的逻辑顺序为
    ) d7 n" k2 e8 z2 P0 ~# G1、最基本的朴素算法
    9 s/ r0 l  j7 h8 `/ Y8 D4 b2、优化的KMP算法& c  v0 l$ S" q8 s
    3、应算法需要定义的next值
    # d6 ~: h6 d* d1 L5 x1 }( T4、手动写出较短串的next值的方法
    7 B# Y  c, e: z8 N5、最难理解的、足足有5行的代码的求next值的算法' `! e: ?# ?9 Y) Q/ w* W# B0 G4 J
    所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…# q* |: H% X# T4 r; v: n
    ( I# }$ Y. I, D+ o, [; S

    7 t- K1 B8 E& y( J9 V一、问题描述/ ^! Y- C$ B% `+ H7 L% Z6 z' _
    给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。
    ; A: R3 k) [( K+ ^- n, I( G' E* L4 j: \4 V1 g

    3 [9 |1 \6 H3 o% F0 T二、朴素算法" ^: S6 W; y5 x9 l6 [' j
    最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:# _  w; A# l' a9 N. }
    1111.gif 5 L. x& k2 b- h9 \1 u$ d

    / y& ?# r2 v$ H$ U/ D
    + h' w7 \3 \# U! }: }9 a7 D9 o

    - O( G' M) s* @这个算法简单,不多说,附上代码; H4 |% ^) `3 o% G
    % G5 Y2 U% G' z# T, b6 W! U
    & G! M! w' _7 k( b) v. w+ t4 L9 B
    #include<stdio.h>
    ; x$ U+ F; V/ L6 dint Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数% P: c0 M% ?6 Z. {7 s$ S& O
        if(sLen<pLen)return 0;& M$ i" e  H7 y7 n* w. I; }' ?
        int i = 1,j = 1;
    + U# _5 J: p$ J' Y    while(i<=sLen && j<=pLen){
    2 v5 v& C& {* m# m* a  B( s        if(s==p[j]){i++;j++;}
    8 @. }; a( U: n4 |, N: ~        else{* V1 ~9 @  J1 k% v( H* a( ?1 `
                i = i-j+2;
    + T" r" X' F- X            j = 1;
    4 i$ z2 \6 ~' Z        }
    , v+ @0 O9 Y+ }6 ^8 _    }
      Q, h4 ^8 U! Z$ g* y& K! n    if(j>pLen) return i-pLen;
    / k* s% B7 t: m5 ^    return 0;
    2 j; N! j4 [; X' [}* H, f9 M9 w9 n5 o
    void main(){4 O+ a8 _/ W  g$ F- x& D9 C
        char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存
    - h" H' D& n) f& D  L6 T& Q    char p[]={' ','a','b','a','a','b','c','a','c'};( _9 |2 o& L4 N; N; b- {0 w2 o( J2 u
        int sLen = sizeof(s)/sizeof(char)-1;  j; {4 Z2 Q) O; k; m$ b7 ]0 j
        int pLen = sizeof(p)/sizeof(char)-1;9 F# N4 O& U4 H+ R7 ?
        printf("%d",Index_1(s,sLen,p,pLen));
    ) A0 B" w; D- z; W}
    5 P6 F5 Q3 g7 J0 u( r3 A1
    7 p$ W: N3 b) b2! K  l7 m7 R5 y7 m) m
    3  [: c$ s, `) r
    4* W4 J5 \  A  }9 E# e
    5. o5 R- L4 r* D. B7 p) m5 E1 ]3 l
    6  ~/ d9 f. M8 D  L1 o( o  D
    7
    - J* N5 U( v; b- u! e! ]  ~8
    ( o2 f7 f: Y5 ?6 p9
    6 |0 b3 J1 s* i1 w$ M10$ L) Z! ]/ M5 H- j1 A
    11, M/ n, G( ~- m' _. d  g
    12
    7 h( B7 K2 W  D% n- [- K- t5 Y13  {  `1 J& c$ x+ n5 J1 k/ z
    14
    1 ]9 W) d( T2 Y1 e* f15
    - |- j% y% w. b4 v16
    2 |9 `5 B: f( z' N17
    + |0 D- |# g8 S  u; t18
    . P9 n+ B: s0 Y$ `' k5 I. A19
    6 g6 k) J! Z" r$ X20
    7 N& g! X; ~, |8 z1 |' f( t2 a214 R+ ~, M) N2 f2 U) O
    三、改进的算法——KMP算法8 c, \/ |) E6 s: f6 |6 j0 A/ b
    朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。$ V* ?  u7 W) U, A4 a
    一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
    0 e3 J8 o7 |% L7 s' y8 |0 d9 A5 U朴素算法中,P的第j位失配,默认的把P串后移一位。: k; ]+ z1 `$ k3 b$ T) B7 J
    但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:
    / E) L2 B0 C4 ?$ c$ p0 j. S" d' q7 b6 q8 P2 U' u! ^% }( i
    222.gif
    ( t% F# {$ P) a( B
    ( {0 V7 r, A4 G" M# _- |, [& l
    * M5 o: X# V$ f6 O& g) o
    这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。( h% h) ]- M& m5 a$ j" L( W) i

      U6 F( P: B$ l6 l! Q

    ) \! h  e1 w7 Z) ]% ^. j相比朴素算法:
    ; J5 P/ j, m: |  w朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)
    * ]$ T$ P1 n; UKMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
    8 a0 ]- y1 p0 x3 I! U* _  Y7 G5 I6 [/ i- `' q0 ~
    - f2 _0 V6 y2 d$ W7 t% t1 O
    而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)- ]" t# J1 ~. u2 r- W6 T$ z2 K
    " u$ |$ M5 ]# K6 X3 \

    2 L: u" o! r0 m+ ?' Y0 Z) _  o* ~开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)
    6 F9 p! H+ K7 ^) i) o
    # X% S# s/ O9 \* J- I# d' D1 j/ Q

      E7 X8 y2 V: d! D比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的
    # p- u" ?1 a) L/ a* A
    333.png
    0 R& Q5 t- F/ z6 ?9 T5 ^9 l0 A+ j. l' |) y1 {2 ~
    5 O0 V3 g+ R# G3 w( J# J3 M
    假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”! t( D3 H% l: a  f

    7 S6 }- ^8 E- q, }) i* t6 Q" |
    4444.png 9 }5 Q% v& @" z

    9 S5 D6 O$ w+ B' \" q) v; g0 B( {1 q

    . k5 U, V, [& E9 D/ C1 B  A' c那么可以推出,P1…Pk-1与Si…Si+j-2( b  ~2 X6 O" Q; `+ \! p+ e+ C, X

    , q4 W7 t, @) `. t: t/ F
    555.png 2 [( [/ I, E+ j8 p7 n2 g7 V3 u: i

    1 z& w3 M: T& ~- H. r! U

    7 i! t3 v+ G6 Z5 E% B显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:
    & t+ i9 d% [' U. Y9 _4 z
    ! m6 G! A0 a7 V# j
    666.png . A) Z* U! G( D; v* q9 d9 u# v

    ; |; e  g2 p$ A! H

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

    . ]6 K5 l( j8 w; \7 W0 k7 E7 w
    9 d$ D2 `- G/ m. D2 L5 S
    最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
    - F0 c2 n# \: f8 d6 o, _4 M8 u  U1 y) x; z4 [: ^# A6 d$ ?
    1 |4 E* Z1 k3 y  d
    四、手动写出一个串的next值9 p& d9 H( T' d* S6 M7 M1 N5 g
    我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:
    3 o+ S: J+ K% k# j$ T% p( u/ ~1 C' E8 L6 s* }" s
    7777.gif
    & x( y. D! t  J) t3 ^- B/ \- W0 A& C这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。" M+ Z3 j1 f0 h# ^: x! d1 A

    * m% Z+ K/ [: ^; J2 M3 z, B1 I

    5 Q1 P* r5 U! e+ @通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:+ L% R8 \/ N- F$ [* T: U8 ?& x
    : X7 @, ]2 |# ~( [5 D

    1 t4 n) C6 E3 ]# Y' h7 A
    8888.png 6 k, E0 z+ {; q1 L
    6 l' ~" |7 Q# f" v
    五、求next的算法
      x1 l  S. N! f终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    ; I1 f& ^% W3 ~2 x: C
    * X6 U) _/ D$ |; n
      O4 i4 v8 u6 e; y) c
    int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度
    - u# [8 f: @( T2 N9 P( Y9 |    next[1] = 0;
    ! U! _% J  b% G2 y    int i = 1,j = 0;
    . l9 ]  ~. O# t0 o! @' V    while(i<=cLen){1 A7 A; ?; \# `+ s8 |4 |7 ?( l
            if(j==0||ch==ch[j]) next[++i] = ++j;
    5 R, Z6 a8 k- K        else j = next[j];, E* g4 N' J3 x% {4 u
        }% `# r: ?5 ^% u& ]) W
    }( l+ m+ r* V$ `. e
    . d( c+ `  x* R0 A! Q9 W. I- x
    还是先由一般再推优化:
    2 K, z1 Q7 F) |  s& ~/ A) r- a+ J直接求next[j+1](至于为什么是j+1,是为了和下面的对应)* W. f: J  W; t: h/ j1 A( S8 b
    根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。' P$ S0 R3 T9 z) |, e$ h
    不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:
    6 D) `  H2 F, I. G& }if(P1…Pj-1 == P2…Pj) => next[j+1]=j
    * ?) @- m. ~% X& r. [. felse if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1' u. E& d8 i" W
    else if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2" A* v$ R7 U* i' `5 Y

    % [/ g! K5 O* t
    $ a* r/ c0 `. S5 R1 ~
    ! ?( Q' Z9 ?- O- Y5 Y' V" V5 oelse if(P1P2 == Pj-1Pj) => next[j+1]=34 b. Q8 ^! T4 `$ m( @  n9 k
    else if(P1 == Pj-1) => next[j+1]=2
    8 Q2 m' }3 _* [1 G$ u# xelse if(P1 != Pj-1) => next[j+1]=1
    : f  ~* @( t! D: `* K每次前去尾1个,后掐头1个,直至得到next[j+1]- l# c: R+ X- `5 m6 @

    - `/ U9 x& ^; x  k

    & I" N. m0 h6 V; M, z再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。0 D4 u2 z" V/ k  ~2 i$ z: H
    但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,
    ( h8 T' h- B% ^8 M0 T- v6 l' L' n①next[j+1]的可能的最大值是多少(即从哪开始验证)/ i" P, [" B7 I
    ②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)/ v; Z0 v0 k& i# @( I7 r
    看一下的分析:
    4 T" T* v1 B% j0 |- R4 ~' }! O2 C$ N' p) e0 m: L
    : ~, J$ n: E" }6 \- m! w
    1、next[j+1]的最大值为next[j]+1。
    - Q' n7 \7 g+ e& w& K! |2 k因为:- h+ q- O$ s- x- Y
    假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。" i3 @0 H) D" X/ L2 B: d% y
    如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1
    + A0 h( j7 p: @2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]
    , ]( g0 @  K- I- P. g这里不好解释,直接看下面的流程分析及图解' Y% [6 B5 G8 p+ J$ F/ t

    6 k$ V1 X+ |) q, o. _

    ( Z: \1 B' _" ?) T( z5 B# Q开——始——划——重——点!% B2 E/ n! |) T2 x4 U: n
    从头走一遍流程
    * l6 Z' l3 Q: G①求next[j+1],设值为m! _" Y4 f# }/ J" B" B$ t
    ②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1% `. q" c5 F8 \7 w! _2 ~9 z. J/ s
    ③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则
    ' I8 }& B' {  [! D8 ]: x④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1
      {1 ?2 S+ m1 ^6 B⑤第二第三步联合得到:
    , o* ?' s6 ~" g3 b2 M+ g2 v( KP1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合7 x5 H+ {2 e! U4 j' ]! _
    ⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
    2 @; W0 w" Z; X( y7 L0 g* `0 T( L2 {& N$ B( Y  L# X$ g
    - h! S* Z! a) W. G
    上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:6 Z, H- ]5 G% K1 }0 f+ a$ g) c8 M2 z
    1、要求next[k+1] 其中k+1=17% P6 }  H2 P" U, x8 `6 C* A
    999.png . w7 L7 |% f& ^  }: o1 z

    , s3 B  p, ~$ w- W# m2 z2、已知next[16]=8,则元素有以下关系:- h  I' E; U/ y: G- O
    10.png
    ) f1 U( Y1 `; `" M3 l: C

    * M6 H5 j6 k8 V3 p( M* C5 U7 i3、如果P8=P16,则明显next[17]=8+1=90 Q+ b7 `  p  g/ |" B% P" f6 N
    4、如果不相等,又若next[8]=4,则有以下关系8 u% t6 M7 k& @- t
    ; ]1 J- X' n8 o+ O
    11.png
    ! U; y7 j8 m/ U/ i# X又加上2的条件知
    8 K" \: B6 K% [: k8 e0 }/ ?9 ^1 w0 `9 ~! h! t+ C7 C9 K) P
    12.png - W3 t8 m- w0 W; ~/ n
    主要是为了证明:
    ' ?6 ~1 Q5 `  ^
    13.png
      k$ d+ s( x0 z0 ^5 A7 _. V

    3 f/ d6 h) j- k: y! g5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推
    & W) W2 P3 J7 {& f" ^2 E5 g) q6、若next[4]=2,则有以下关系
    " T! [7 W( y( Z/ {
    14.png 8 \* u$ a5 V0 C& y

    7 Y$ U# m( o! u! E8 E; H& N7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!& S) F4 s% _, q2 P0 A
    ————————————————
    0 H& \: M& ?! G7 W" [版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。3 U1 M! {+ R& J2 l% R' V! u
    原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411
    - P* h/ v* u' E1 X. k7 v4 z- w
    2 m1 w. q  _3 G) F2 d# y" ^( X& z9 ]
    * k) u3 k9 g0 c7 @# ?7 q  N
    / P7 [) R. Y5 S5 u* Y
    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 11:42 , Processed in 1.161913 second(s), 53 queries .

    回顶部