QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2877|回复: 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值算法
    7 ]% s+ `, T8 y0 c' B; `
    8 {. f9 M0 s2 k9 e大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。3 B- ?" }% R8 h" M- w2 N
    5 _% b3 ^7 [* z( c7 \# h6 [- Z

    $ O6 u( n1 G; C8 K本文的逻辑顺序为" V& {1 a# a( v
    1、最基本的朴素算法9 H3 K! ]0 s2 M6 \% w" c0 [! q* [
    2、优化的KMP算法7 L$ m( i9 C- s/ Y% z1 e' _
    3、应算法需要定义的next值4 T2 h$ w- _+ u
    4、手动写出较短串的next值的方法/ [* O2 c6 Y. G3 s
    5、最难理解的、足足有5行的代码的求next值的算法( G% ^4 \5 k( m7 ~( v; z* F* r2 ]
    所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…
    7 [6 P: t. e& n8 g# w; N
    4 ]6 V8 B5 x4 Q1 M- ?
    1 b% m+ T: d# [$ u. n# y) u0 N
    一、问题描述# G% m- d0 K8 o9 Y. c
    给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。
    ! C4 \) w! ?6 B( S( b. j2 V) G. g  G' j: j& Y" V/ Q$ \& S
    0 s, }+ s" w1 E  B9 q- `# [% ?
    二、朴素算法( W7 w$ i- F% g9 i& j
    最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:" I, E( q  q) m* {
    1111.gif + o7 ]. }! M* r4 n( V* n

    - Y0 d+ h( q, V8 p* l$ V; o9 O/ i$ T& D5 {% J: c2 @
    . R) D1 \% t- v+ S: j. H+ b
    这个算法简单,不多说,附上代码
    3 q" ]+ C8 R9 j7 e, ?* w. O2 a( A" [3 U+ ^
    8 d% l) Z& B, s; p  I' F! `
    #include<stdio.h>* d6 k$ E) U" E0 A$ z
    int Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数
    , P' O0 ^* r) ?+ `% y0 a, M    if(sLen<pLen)return 0;
    3 X6 |, v8 j& C) ?# B. g5 d. E0 T    int i = 1,j = 1;
    ( n$ v* ^3 N( Z0 N& m: M    while(i<=sLen && j<=pLen){5 A" X4 ~& n/ r! ]
            if(s==p[j]){i++;j++;}( J& s8 W& v& l# Y% j) \3 k7 f
            else{
    ( _& A4 Y! V. G+ A- t0 |  Y- G/ Y            i = i-j+2;0 L. M: E7 @+ l5 H% X, D
                j = 1;( {/ w- M6 k; w
            }
    , ?! g2 p! n+ ~    }/ R& _. u( y6 V$ A
        if(j>pLen) return i-pLen;
    " l; z3 p7 V+ C- c% o    return 0;
    ' _: ^( t4 k* F  R5 r}
    , s8 N* \; P5 O  `) Y5 Q5 m' kvoid main(){
    + f0 Y' Z9 l5 J9 W! H9 V  L0 {    char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存
    / {; ^; T6 a' _1 D& x    char p[]={' ','a','b','a','a','b','c','a','c'};
    9 B. Y8 \$ `0 u% D& ^    int sLen = sizeof(s)/sizeof(char)-1;
    " ^+ v' C1 |. q7 H/ N7 [) R    int pLen = sizeof(p)/sizeof(char)-1;, H* ]$ v' o3 i8 H; v% R9 q  K
        printf("%d",Index_1(s,sLen,p,pLen));
    & e/ u5 n5 v& Y2 X8 [}
    ' R: I$ ?5 Z- M( X4 N8 z2 R$ ^* J1! G, l- `& @9 R. C
    2
    " ]- W* Q7 {, _; \" L* a# ?9 b3
    8 r# |8 A  X7 k0 {# Z4
    $ Z$ z. a! D$ q5 P5$ E/ u  Y$ p  X1 A9 j4 A1 W9 {- S
    6
    9 t7 w2 c8 R, |$ y9 F; O3 @7
    ' J. G. l, q/ |! J. `9 v83 H& o4 f' S6 X
    9
    5 I: \' C( i7 A# |7 j4 X6 D/ E10
    " N5 Z  [) c1 ]6 O' j) O11
    ! j% k- Z( `: w6 j" o; U12( C5 o: }. F6 a' b7 d% q1 A$ h
    13
    ' w9 Z4 k. r5 N9 v8 s14
      q/ A% m0 ~  w15' _5 n. U0 b5 l
    16
    2 k" v: U7 N" o- p( q% i1 ]17
    + q8 |' z) D6 I, `18, J" d& ?; y+ ~1 d& D/ n( f( D
    19, e& c# ], ?+ s, J  \  \
    20
    8 a' G1 j* I/ w' v/ O; l' x21
    . b$ ], u0 f7 T9 Z/ m! G三、改进的算法——KMP算法3 G# i; C% F: F$ _# j  F/ t
    朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。6 f* A2 J  G, z* N5 R5 H
    一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。- Y, X! q$ J1 W) g
    朴素算法中,P的第j位失配,默认的把P串后移一位。
    % W) {/ m) u# ]' j' R' y- H但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:- u* D  z/ X+ i; W  K
    % {% g/ I2 e( H( |
    222.gif + ]8 J$ u; M$ R

    ' r& n3 q9 B( `- W  O& H

    / D3 S3 U9 D/ y# V* h这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。
    2 D( G  ?( x  J2 U' \8 v( V; @6 @
    # M, S5 l8 [$ p9 {2 \5 I
    相比朴素算法:
      g* S$ G# H# Q: @  v朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)
    ( v$ t( r5 F( B$ U% i$ S% HKMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高% ]* A! M1 y7 t

    - c' x9 N& q8 B* F' B: R. X" c% [

    ; z9 f( U" ]* O* \1 s6 e! |而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。): [7 l; _9 L  I+ ]
    7 X% [( _( P6 g8 d* {7 b* m* L, n
    , P$ S+ t+ A* \. e  ?- g
    开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)/ Z8 o2 F% n$ R; M

    0 E7 i! P. }  p' w% M/ ^( j) b- b

    5 H, f& f- E) }比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的
    3 y8 C' O+ y1 e, }+ }! U% M
    333.png
    ) E; H* D: P( ^# T1 C
    9 F7 E0 P5 H! f0 |' n% X

    6 i, u3 c" Y3 Y% O+ Y5 U假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”
    & C- F+ A2 q6 y! y9 B3 \6 j! I( i; z- i  u8 z3 o
    4444.png
    . N2 |) o* f5 M/ z0 R
    ; ^1 y. q. R2 j* Y% t# C
    , {& ]+ a7 i, z
    那么可以推出,P1…Pk-1与Si…Si+j-2# @( E# s2 l& a6 Y4 L6 x9 F) {
    " t- _% z3 Z& M5 m; v  P
    555.png
    + h! L0 ~0 c) X' \; n
    9 c/ F4 B, w3 ^3 I
    & d8 D9 R' V+ ?. s' @8 f* w! M4 }$ p
    显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:0 x1 H1 a5 U$ j! g1 B
      ], |) m3 z$ Y: G( \$ B! \7 _. z
    666.png
    ) h, h, o+ ~* D3 t% c
    & T4 o' M. ^# s6 ~* a
    0 r# Q8 P  {% L" G( O' V9 A9 H9 Q
    为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。% W5 j4 H, x( N3 B4 v

    * T- k  H; @/ R0 I  C! U

    # L6 V$ L7 q# L1 e  e! D最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。! o# M: ?, t% a: ^' F3 A
    / W9 t0 p; @) L

    + b, u- O. E, h4 ?+ D5 c% \" z四、手动写出一个串的next值0 ~* P+ {$ f% [, T2 x. {
    我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:3 [# \4 g: t4 B6 J2 r7 _
    ) J, G6 d# y" U9 H: R
    7777.gif
    ( p7 d* z1 O4 O/ I4 L4 _2 v" z这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。
    $ I# Q  s" |3 ?, _$ t+ p) A9 ~4 V$ o
    5 u9 `8 V) Q6 M. w6 M  A
    - h& S" D/ S# W1 P% D0 u
    通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:
    3 F% z( ?- X6 ]% {" i0 F# U, G+ y, @, d8 C
    6 T' [+ x: j5 n$ l& y0 Y, g
    8888.png , B2 L* C3 g* j0 ^
    - |; m- E0 Q9 X/ u+ C1 L3 v
    五、求next的算法5 f1 |5 i, \) u; n& s$ d
    终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    5 Y6 Y) f, \: J( G4 |% Q( d7 S
    4 i" `7 O! ]$ E, n" @0 K

    4 A8 [0 N4 R+ f7 B/ `' Dint GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度
    ( Y, C& h! I) \+ y- s2 n1 _    next[1] = 0;
    ( p; G. a% X. L  ?" K* C% l: l+ S# J    int i = 1,j = 0;+ m& W6 `( L. `4 F: O
        while(i<=cLen){/ @6 r, y8 Z. T+ `7 C1 U( s1 `
            if(j==0||ch==ch[j]) next[++i] = ++j;
    1 l  G4 H6 @# V% v4 s8 Z9 G6 d        else j = next[j];
    * ?# _. N3 R8 l0 n    }
    1 F9 u- C' e1 t5 x/ L% H. O}
      {3 I8 g' @2 g" E% _) N' j5 B
    4 A  k- F3 g8 v还是先由一般再推优化:
    3 Y2 b8 E' [. R3 J" Z直接求next[j+1](至于为什么是j+1,是为了和下面的对应)+ S- S1 _, Y! ]: `* y  h  S
    根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。5 m0 D0 w( ?8 G
    不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:
    6 i0 w1 K5 Y: W2 vif(P1…Pj-1 == P2…Pj) => next[j+1]=j: r9 f" j( e- z! P
    else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1' T4 V* F- }. i2 H9 L7 @  M
    else if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2
    / D/ q* o! ?: s! m9 K/ t. S8 E, N4 ?% `. [. N

    / x% t; C' U3 }7 ^. s2 {) ?* r/ Q' z  g5 }( |' M% m. l  N
    else if(P1P2 == Pj-1Pj) => next[j+1]=3/ f/ x& `5 W9 i; c/ T( B; F
    else if(P1 == Pj-1) => next[j+1]=2% h0 \8 ?$ K7 d, l9 m
    else if(P1 != Pj-1) => next[j+1]=1* ^& t, z/ e: r
    每次前去尾1个,后掐头1个,直至得到next[j+1]
    ; Q0 k" [. F! M! j4 |5 w8 ^9 ^# Y4 H; T" p* B/ j
    ) o' ]1 K) y$ Y2 U$ P( N3 G: o. T
    再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    : R" g6 u0 c8 `' l8 a4 ?! \. Y但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,
    ( }( X$ l0 n+ \6 K: C/ @4 A①next[j+1]的可能的最大值是多少(即从哪开始验证)/ j7 J% U& O% l' _; D6 j
    ②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)2 g( M  n& C. o
    看一下的分析:
    : E, v( g4 {  |; M
    + R; s$ s  x- I7 u) N
    * T" y1 L, T$ D& [' V" r3 U
    1、next[j+1]的最大值为next[j]+1。6 H) g. ~7 j3 O: A) p
    因为:( D+ q: V8 E" i. ?
    假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。) b- W$ t+ ^3 \' Y$ a) i' r) E6 g
    如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1
    0 v8 o) i6 N4 [- z5 n4 C: m2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1], @2 D5 P8 u. i8 ]
    这里不好解释,直接看下面的流程分析及图解+ o" `% a' V5 b0 f% g9 X7 b+ p( T

    ! x; c0 E3 A6 O8 Y
    9 l+ _# Q5 H* u
    开——始——划——重——点!# p& Q) `2 [/ h$ C+ X4 u5 B
    从头走一遍流程
    * }$ b; `7 Q- N; L- m①求next[j+1],设值为m' a4 n2 y8 Z+ O6 [& N
    ②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1
    # B/ |7 H3 ?+ G4 ^; q+ [. \③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则
    : L9 N, S+ N$ i/ x8 B④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1/ e' S) Z- \& w: j5 g7 `
    ⑤第二第三步联合得到:
    & p0 l0 ~  G9 r) u+ sP1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合
    % a3 U4 T  _% c⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推9 t3 N. }/ b2 s+ c3 V2 H
    + [+ l: M" }% H# v- \1 x, F- [

    * q7 ]1 S1 u0 n8 F上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:5 S, P3 j; J; N# z5 V& ]& E
    1、要求next[k+1] 其中k+1=17
    2 C! ?1 C( `1 x9 \  w% T
    999.png 1 `9 k; ?* L  \% J
      g% f! h" z5 L. c9 H" J% w- ]
    2、已知next[16]=8,则元素有以下关系:
    $ `3 \6 V( F4 P+ w5 }$ T: u
    10.png - v. x9 E9 {5 I$ z  u7 f# R6 S* _5 \

    ! I6 w! t. ~8 `! d2 c3、如果P8=P16,则明显next[17]=8+1=9+ N) T$ i1 [' d! |; T& c. [( m) ]
    4、如果不相等,又若next[8]=4,则有以下关系
    0 R6 t2 y2 Y4 B/ X6 \" W  ^% R. v" o
    11.png * N3 M7 U/ Y1 D4 c% Q' {2 O
    又加上2的条件知0 n4 @3 T' R* E' d- J( o" V
    2 s2 P, U2 }7 ]3 u$ \' y
    12.png
    - ~( R& {3 c/ d6 [主要是为了证明:$ P( a- z: D$ G5 J; _; l/ F& y
    13.png ) @& p& c$ A0 c+ b5 s
    4 k5 H- R, v7 z9 V
    5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推
    % A% j. _$ s5 E+ w2 P0 h7 x  y6、若next[4]=2,则有以下关系
    ( x0 Z) I6 J1 e: X! a
    14.png 9 @# F: k% o* B& A
    1 X0 Y- q8 q8 W5 |+ [% U/ i- b
    7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!, k- ?& z1 ?1 ^; h. U( J7 f( i
    ————————————————) u: i+ I9 V/ k
    版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    " P: |1 R/ E% f, k' B/ H) Z- C原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411- [, a4 x0 U$ Q3 ]
    ; {% Z% b! _6 u

    . g) T: _; n& B0 T2 }; u9 I

    - ~' \; M8 ~8 |& l  T, L; S  w$ A
    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-13 23:32 , Processed in 0.438064 second(s), 54 queries .

    回顶部