QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2932|回复: 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 l8 C6 Z0 g0 w+ z6 M% p' W( ?6 M: @. G5 r9 J6 e* H+ O
    大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。, G2 h3 Q! k7 H2 ~5 P
    3 Y0 u& H5 [$ l2 ]0 _/ ~+ P
    ! F, }$ d( ]; k) o/ d9 W2 X- U- v
    本文的逻辑顺序为
    7 U1 I% G. j# P( d8 s1、最基本的朴素算法
    ) w2 S! t1 m( [% x5 \2、优化的KMP算法
    * `1 L$ U! Z! Q7 o* D( d3、应算法需要定义的next值
    ' W8 ]0 k/ a) |* j. C  d4、手动写出较短串的next值的方法
    & [; }7 h, J8 b! S8 ^5、最难理解的、足足有5行的代码的求next值的算法- A# m6 y( ?% p% w
    所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…) Q: r1 o6 {- O7 Q
    5 Q) C& N  l: P) P

    5 y8 w% p! c# x8 Y) [& B& k一、问题描述5 ]4 I" @  \% [5 J" l
    给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。
    2 H: f4 [+ x/ a; R
    & m/ ?$ D% Q! {6 J! z8 k

    % p0 u3 k# c. U) X2 |' H' |二、朴素算法
    ' l0 i7 P$ b2 f$ Q$ |. |/ I9 |* p; h最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:- R6 s4 x5 B+ Q1 H' G5 n8 u! f
    1111.gif ' V7 O7 l+ H1 f' H% A3 C

      z: ^7 \. s! w* h* U1 i$ l  ]- [+ E$ n! x2 J8 y6 M2 a' y

      T- [0 h, V( V. ^0 J这个算法简单,不多说,附上代码  m- f& f, K% [3 r, ~
    ) ~: [! E) F( p. q( t  L  N
    ' x; d$ _: _3 ~' r) |
    #include<stdio.h>
    3 j# |% }: F6 a% j. W3 hint Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数
    - b4 D9 F9 f* c9 S; S0 ?* G    if(sLen<pLen)return 0;
    4 F. a  ?! V6 r0 c    int i = 1,j = 1;7 V' i1 {  b! O, x0 n, _
        while(i<=sLen && j<=pLen){
    # c) R% K. S8 \# B: X- U8 V        if(s==p[j]){i++;j++;}
    - O; m* u! n" Q+ n+ B7 D% v% i5 T        else{, x. r3 s% Y- W. P
                i = i-j+2;' W9 _, |: K5 F0 d0 f7 O$ I
                j = 1;+ Q5 a9 P/ A- g- v+ Z' ~1 z
            }
    ; w+ Y4 Y5 R, q0 G6 ^2 n    }4 G6 p! M7 o' {! I* r& z
        if(j>pLen) return i-pLen;
    ; D& _/ C# L' I& A& N& V    return 0;
    + C" I8 h5 s# m% |( ^2 W}
    3 r  U' q" f' m5 b, wvoid main(){
    / Y/ j2 Y/ o; m" D, j    char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存) }: y: T5 t8 t1 J! ~: j" m
        char p[]={' ','a','b','a','a','b','c','a','c'};. V4 T! Z7 y7 W5 X+ l/ Q
        int sLen = sizeof(s)/sizeof(char)-1;" z# i3 a8 {+ ~
        int pLen = sizeof(p)/sizeof(char)-1;" O1 w! Q3 W5 u9 ]( ]8 _1 K
        printf("%d",Index_1(s,sLen,p,pLen));7 O4 t! M0 l; q; o- z
    }
    3 k! a: T; J$ c8 S$ g9 P1) u2 I5 U6 x8 R
    2
    & X! a( n4 h0 o; D7 f38 B/ e5 y: M1 e6 t7 C# i
    4
    7 X8 \/ B* `" n52 q7 K# d+ O! I( C
    6
    6 r7 d$ v; Z9 E% J5 H3 h1 \7! q: U) l4 o& I9 G9 l. I; P
    80 C, K/ p" y& Y. w0 U7 Q1 d
    9
    7 t* e/ n" `  v$ Z10
    & n! E1 h: W" i) ?11. o& H! r2 h, N: Z8 M) \
    123 ^& w5 ]  @  A. G
    13
    + V# C/ E) @! {( M( @7 v14
    ' z$ E  ]2 x- J2 o- r# h7 c15
    & K1 T# x" @; o1 w# ~16
    % j- T" [$ m2 M, J179 s& i3 }9 m  g/ ]4 r  b; V5 J& N2 c
    181 s1 E& F' @4 v6 o
    19$ G" W5 r3 M' A
    20
      L: z3 c$ E' |8 n# c+ g2 x+ ~4 ~21
    9 w. w# F  E" [) S- |& g三、改进的算法——KMP算法) U3 y% B' H% A- Z3 S
    朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。  S' A: E& r9 t4 O' g+ X0 F# K3 Q
    一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。4 @7 g' I& i5 x+ U/ F
    朴素算法中,P的第j位失配,默认的把P串后移一位。
    8 @+ d/ I8 B! K但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:" {: ]2 q+ [: P" Q. X: `0 T! Z
    & w/ `1 h3 k0 f: B1 K
    222.gif
      |8 Q  l8 L4 `  \0 p+ v2 W& S# G0 L: k/ }* E# d9 S
    * K9 h0 p; e) H4 a5 v. q$ {
    这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。$ x+ G( G% X! ]: u5 s" c+ J) C* s
    5 n2 A3 W. p9 C7 h" l1 w" q: r
    8 b7 k! n' `8 ~( c$ l/ P* e
    相比朴素算法:9 ^5 m7 D, V+ E6 q- Y* j. l! w
    朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)' {: a& Q  N  g2 ~! r; P( W
    KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
    0 S3 p6 a2 E8 [1 Y* _/ i
    5 `1 B  }  ]; \  [! S  v) \
    . q( O2 V, N3 M' u- C' F( ?9 E' j( U
    而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)
    + B0 B0 r" N2 W5 v% Z& u
    $ A9 [5 ~5 P2 x/ ~. m2 W
    ! y' @* Y+ h5 Q4 ~6 C- H' A" _
    开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)# n9 N3 K% k+ {. Z
    ) \) L' m, v- B/ c( ?* q' Y+ ]. O

    8 ^/ P! G* l9 w# c比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的
    5 {4 A0 r+ H* p" \
    333.png , z, A9 s$ `: Q  c. \2 p

    + |# a, L1 o/ ~6 A

    " D! D- \5 |2 f' q+ I+ W, p6 @, @' r假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”/ X* }4 q" H8 m/ n4 i5 K* S+ r
    ! ~  I8 J& b8 n, `
    4444.png
    ' j1 c' ^7 b- k: a  Z
    . f0 ~6 E8 {/ [+ q( k
    4 y3 c0 N* T; f
    那么可以推出,P1…Pk-1与Si…Si+j-2* @0 P' a7 [$ ]- }: j& l
    ) j: [- |& D# G' o( @* n& W
    555.png
    % A/ Z) f9 F& F4 v6 U; X7 _2 j
    $ T& W1 `4 L0 e
    ( m6 f; ?: W% i$ ]3 C$ s
    显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:) \) G0 H$ b5 ~. O. V1 i
    ! b% y, e* ~" S" s1 e& ^  ~) p1 ]
    666.png * @! `1 e' u) u; H7 Z' n. c

    * k# M, K9 J; |2 Q) i
    % j. Y3 d- a' v; L4 o
    为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。8 C+ j1 {: j( F2 m& N$ Z- V* t

    7 t+ i) w. B8 p+ D. `& `

      N# T2 o  O* a- K最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
    7 |0 Z; O5 v# y; r* ?- V2 V7 l( r% t  p( d
    8 V9 Q4 K% {, b% N% U- o
    四、手动写出一个串的next值# e" y, n7 I# r* N  Q9 s) _! N
    我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:0 `8 V  r* w" T7 p

    1 A% d2 x% h4 H
    7777.gif
    0 v/ J& D& e) v, p4 G" @这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。
    5 x$ Y- A2 [. n( g! n2 ?
    2 d; P0 g; X9 A% o. L# Z' d
    ! p! E% N. d, m: N
    通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:9 a7 R3 r. P) f' j7 B0 s

    5 `6 l2 v$ H4 J
    4 D2 y% k# Z: W2 n* L8 Q" H
    8888.png
    8 T0 \% B: s, H$ V) s' q- Z
    9 t! o5 {# w6 s) N1 t/ y2 u
    五、求next的算法# R1 ?) G' @: b3 x
    终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    ' |$ V4 F% ?. b- w; S" r4 a1 u: r% z

    . @: `) c1 C* u8 A9 j  W" V# Rint GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度
    6 f# R' s/ G7 q6 y6 l    next[1] = 0;: \# x+ k( F) `$ ^% \: [
        int i = 1,j = 0;" z5 S2 f' p, \# K! ^# o
        while(i<=cLen){, t$ e' o1 A/ M7 O% o$ @8 V
            if(j==0||ch==ch[j]) next[++i] = ++j;
    & X# G3 Z& ~. }; n- j- d1 ?        else j = next[j];7 R. e7 _9 T: b5 `9 \
        }6 t+ D2 F4 f& a
    }- ^, q& ?1 `+ W- j* U

    3 Q  x7 @/ {. b- d- @. F还是先由一般再推优化:, c4 C! X  l$ M0 V0 g$ u  W. ]" |. G
    直接求next[j+1](至于为什么是j+1,是为了和下面的对应)  d1 |6 E( ~" Z# R  {; ~
    根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。- g) j+ W: n. P
    不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:# s0 m  G, f8 u" c# L
    if(P1…Pj-1 == P2…Pj) => next[j+1]=j
    * R4 W. h! b' f& _9 Y" selse if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1% |, ~8 @- `; l: z/ z0 s
    else if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-27 U& a4 O6 F* A$ K8 a
    # O- |4 {, Y& e% l
    , @( W/ G9 n" e5 O/ c1 t, D
    # F3 c! M4 |+ k( E  m; u
    else if(P1P2 == Pj-1Pj) => next[j+1]=3! U# u8 |0 @* P' A5 k) i
    else if(P1 == Pj-1) => next[j+1]=25 x1 n, n6 z2 A1 ]+ O
    else if(P1 != Pj-1) => next[j+1]=1
      R+ c8 a( @, [: ?# M$ B" O每次前去尾1个,后掐头1个,直至得到next[j+1]
    5 }2 k8 e7 f0 f* R7 Z6 C# n! d) r4 A. F1 S( g

    / L- x% f: k" o/ D' ~再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
      q) q1 W' F4 g8 x1 |- y1 P: ^  n但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,
    ; P+ z; s, L- l) B* P4 C5 w& t①next[j+1]的可能的最大值是多少(即从哪开始验证)2 B$ x9 N1 t, R7 `
    ②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)$ |% H# E3 N5 H) ^% ^$ t0 }$ f3 \
    看一下的分析:3 L; P" O5 K2 S) C0 G

      M: {7 S, N3 A5 o

    8 B- i, c' x2 B9 y4 B1、next[j+1]的最大值为next[j]+1。7 H) ?) P8 _% H2 |& T" x
    因为:
    / y5 {# l, b& [- g+ b. \假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。8 M  Y. f1 K- k3 v& D5 c0 ?
    如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+19 N3 e* H: k/ ?4 t) Z
    2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]
    - L; N3 B4 O( ?$ }) l% t这里不好解释,直接看下面的流程分析及图解
    % ^# q& E& _5 T: F: T8 R
      J3 w; j6 R: _9 ^2 Z

    ! c) m7 ^8 e2 k( s- z$ D9 R" J- K' i开——始——划——重——点!( K9 L3 @- c2 T* o0 F8 y
    从头走一遍流程* a& P5 }! e# \5 a: n
    ①求next[j+1],设值为m
    + \- X& o; M! `/ W( Q! \②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1
    0 J; b4 O+ \# V. N# ]③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则
    2 d8 P4 h$ C; @2 u+ c4 g④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1+ {& i* P6 O2 p- p& ~1 t
    ⑤第二第三步联合得到:; x- Q7 r8 G% s! P
    P1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合+ [7 s$ C6 u) y4 T- L3 b+ u7 n' R
    ⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推2 \5 y: @& Q3 A1 O2 a$ |
    7 N4 c6 {1 \5 q' L( M' A6 G
    0 r/ P- k" H: z7 ^5 j, Q* P
    上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
    ! v* a: k1 G3 H, e% B) ]. c1、要求next[k+1] 其中k+1=17
    / T- c2 U# c; \* O
    999.png
    ! i! W! e* [) w( j# ~* o

    + U+ [. y1 T: j+ ~$ M8 @* ]2、已知next[16]=8,则元素有以下关系:& f" q& W8 X: L6 v% s8 F7 z+ `
    10.png : Z1 m# c0 L" S, w

    7 ]! p& I" V; T1 d9 X8 f3、如果P8=P16,则明显next[17]=8+1=9
    : @6 N2 i3 D4 n0 r/ h* A4、如果不相等,又若next[8]=4,则有以下关系
    ! d# j* j- B+ F; B6 y( f0 ]7 X3 ]2 a
    ( W+ @8 A/ ~0 i5 `2 e4 b7 x6 f; Z+ J
    11.png
    ( V; m3 v5 Q! I; J# n. k又加上2的条件知4 M6 t* ]5 k% n0 v& v) i+ e  y

    1 x8 N- d) A; d0 y, s. G# p* H
    12.png
    # S2 [3 V. h3 N9 o& P主要是为了证明:
    : u4 B6 `2 f; H- \
    13.png
    ) d+ Y4 |( Y* v. h. F# u6 r( N
    1 t9 M, e1 U+ b6 D
    5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推
    9 u. F) }( ]& p# I* j6、若next[4]=2,则有以下关系
    1 u. X4 x, S) _& T
    14.png 2 V1 Q+ x, X5 m+ o
    6 ~6 \- S! M( q7 v( [" Y7 }
    7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!! `# f) A2 j, n2 E: b
    ————————————————* o1 v; |; k. [% g% \0 V
    版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。1 E. [! ]8 T* [9 L& K$ ~
    原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411$ ?7 h' G6 h5 q4 k: P

    3 M1 f$ \' \& c+ }# H. E
    5 ]: |, ?/ u: y) M
    % N/ b7 t1 E, G
    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-28 19:55 , Processed in 0.460870 second(s), 54 queries .

    回顶部