QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2898|回复: 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值算法8 ~3 M' w6 x2 V5 @/ G4 Q  [

    6 M% P+ S( }( U1 i- t大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。% E( F9 n4 D5 ?

    2 R8 c7 `* U1 ^3 y; Z9 D" K4 ?- ^' P

    6 n0 g( X1 |/ G7 r/ B  u本文的逻辑顺序为4 j- E" d: j) c; k1 N% u8 k% Q
    1、最基本的朴素算法
    # u0 A" |; L4 M8 ~  K3 P7 x2、优化的KMP算法* {+ s2 |1 @2 i1 D% ~
    3、应算法需要定义的next值$ c  `! g# ]: ^% N6 k( O" s$ j
    4、手动写出较短串的next值的方法
    ( ^! h2 M8 L1 @# d5、最难理解的、足足有5行的代码的求next值的算法* \$ Q# O- j( r0 |/ y) u; @& B# s
    所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…
      V" T% b  n" p
    # z! M5 z+ z; C9 t- H2 a* {
    0 e8 ?  V8 w8 @. O  B8 ]5 k: |' A1 S2 h
    一、问题描述' d7 f4 ?  L' n$ p
    给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。
    , p- L# ~: B  V0 ~5 K3 Z6 v
    + C7 o3 T4 P& C: B. S
    / p( Z$ P6 G! n) g1 j
    二、朴素算法
    # r5 h( B% p& M2 Y6 Y6 j最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:+ u* p" E2 O) ~( b
    1111.gif 6 q6 {1 f; u  b" J  E
    ' M( z# e3 E: U$ {, v% [, y

    4 x4 Z% p& i: A) F
    ! s7 d8 `) l0 u* T9 [% F% d
    这个算法简单,不多说,附上代码9 _8 z8 p+ ^! i! n9 g8 X
    ) K; N7 R  z9 x, s
    / s' g: L+ f5 o9 n0 a' ?
    #include<stdio.h>
    . Y/ c+ _; L0 w# H" `+ Kint Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数
    " Q1 ]% y% u) o) j2 c6 Y    if(sLen<pLen)return 0;: S0 I4 f0 ?! p) \* G
        int i = 1,j = 1;1 ?* [9 X2 A0 ~, K
        while(i<=sLen && j<=pLen){
    1 M7 R) b' K4 j; K  I8 C        if(s==p[j]){i++;j++;}
    , ?$ a( O* \0 Q! e        else{
    # q  h$ |6 N) d# W. ?5 T            i = i-j+2;
    & d) c& y' I/ r- x6 S            j = 1;
    , W9 k( ?8 p# a( u        }' |4 l; N( r. z8 H+ B
        }
    % K  n6 i- d3 x& ~6 \; E    if(j>pLen) return i-pLen;0 l$ o% s. n' Z0 l2 t
        return 0;
    1 \4 ]7 e  ]! p}
    4 b) q6 l8 P9 svoid main(){. R9 d* ?& D- W7 z& b+ {
        char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存& n. G0 e$ F1 m8 L
        char p[]={' ','a','b','a','a','b','c','a','c'};
    : e/ p) t0 u! s3 G! k$ F    int sLen = sizeof(s)/sizeof(char)-1;9 y3 y: m) a: ]+ K8 i
        int pLen = sizeof(p)/sizeof(char)-1;
    * e- V% s% S2 n2 F" x* ~    printf("%d",Index_1(s,sLen,p,pLen));- ~- o" C. J9 W/ \2 T
    }
    : z0 U; o: T2 K% ]1
    1 V5 J) [- l# S7 e23 n/ a8 m0 z1 G5 B/ \8 F% v& R- {
    3
    ! D9 b# ^7 ^9 J4 ]2 I8 ~4! c: x4 l# Q& ]  T" P0 u& l' G
    52 c! h# @: }2 m7 V3 [5 N
    66 D* ]# I( n5 ]
    7
    . i% r1 I. J8 X0 `8
    ) ^9 a/ R! S# W5 o9" K4 @. p  t7 V8 J3 e, A8 T9 b, F
    107 w' e- r0 c3 U1 T7 N$ S, A
    111 t9 X2 N! v& D/ Z! y, B
    12$ z* j* i% m9 Z' R# Y0 c; R  w
    13
    - R5 B( R; C3 h+ Z4 j149 y, ^9 C8 k1 o9 t
    15
    9 \+ U  E: L4 ~6 G3 C. h7 @2 r16! W" |+ b. r# O/ d" H7 A) M
    17
    9 B5 r6 \. f. ~! s* d181 k6 U( D' i& `6 q% \
    19
    4 j6 O% T) k' X+ ^" ~' |200 p7 H6 C( R1 ]
    21
    1 Z/ L; A% R' S+ ~: u$ L三、改进的算法——KMP算法
    . J! F% t% l+ h朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。; N9 B8 O$ E5 L
    一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
    - K  }. Q, G9 f4 X' f% d2 ^( }! p朴素算法中,P的第j位失配,默认的把P串后移一位。
    , F; P" X+ W' n( E( x但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:
    3 f0 Y- i, d5 l$ ~7 u
      {* l) k7 q: [# v  M% ^# I' H
    222.gif & n/ }' \( Z1 V+ W( o5 K

    5 @- _& Q$ a/ G! G( r2 p
    4 Q, p7 u0 a5 c, ]* @" P, |! L
    这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。
    % M7 p- w  X' R' B$ z+ Q8 ]
    * j- y/ w5 c3 h- A% C

    " t1 c$ n2 Y9 W5 p相比朴素算法:
    ! _/ \" w* ?  b& G* K9 b朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)
    4 G! H- w: l$ u' K$ i! }KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高' A2 _, K- A( V; {  }- o1 L
    1 F+ N, ^2 A& d# P8 Q: N
    1 d0 ^, j" s/ |
    而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)& ]$ E8 G; X2 M* R! e9 G, v

    ) \2 F% i5 J# @$ h' c
    6 g7 r0 r0 C, \6 O; J7 I
    开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)
    ; h5 g6 a& L& i0 H2 f0 ?  B6 _+ a2 Q; \

    % C0 ?7 v! i3 S! d9 Q. \比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的+ ?9 @& p0 A4 }+ B0 [$ H
    333.png ' w) X7 D' F, p5 Q$ i8 @

      r" b$ b) P; N
    1 N# R$ w2 Q" \: G# @
    假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”
    2 n. Z/ B! g8 x+ M" Q0 [) V, }3 V# s  v' E' f, H# e! j
    4444.png $ I) E) H" y1 r% E) s; N/ A% t
    : H5 ~4 g4 B7 C+ e+ H  @
    6 N) @! \* M7 x% L1 E0 o
    那么可以推出,P1…Pk-1与Si…Si+j-2
    / B% h; @$ P: _, F/ G( ~  i  E) f/ V/ y  l( Q: ~
    555.png * d6 o0 @) O, k- i
    % g8 g% w2 F# a5 e, W9 h

    1 X; u# P: [; M. c9 h/ Y3 Z. m显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:
    9 \9 t4 V8 L% Q; X, ~$ B4 j- W2 O5 n1 z4 ~; F$ \" q
    666.png - s: u; X9 F: G" I% M$ T) Z

    5 ~- P! ^6 x; R5 d0 n8 [! Q6 i. ~' I
    * ^0 f! K- F. B% c
    为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。
    0 |- o( a( a+ T. ~3 q
    5 y; h1 o- j4 x9 ]+ U! N4 \

    5 ^* @) u$ ~6 r2 U最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
    ( Y6 H( g. d& o: p' H8 S: `  X* `8 |: N: e
      ^# M( s' w4 l5 A  c+ z% x3 ?: b$ u
    四、手动写出一个串的next值
    1 A8 ^- d7 Q+ E4 @我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:
    8 N  [0 i/ U- t9 Q
    $ m" b9 P  }0 l) y# _
    7777.gif
    ! p& I% J0 E) B' F- {4 U  I( Z# C) M, M这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。+ _  z7 B8 Z1 Z! {" d
      A0 F. K+ @( {5 s( H/ C
    . W+ t' |2 P. L  V0 o4 e
    通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:1 l0 |. G/ [- [* J2 E# b) d& ]* G
    : N8 Q7 E% U) G5 }3 _" o" j
    1 ]# s7 ?$ K. L$ z5 Q8 G: G
    8888.png
    # q; Z3 t# d1 g% S) n! R

    / I' B. e8 J6 C! w五、求next的算法
    ; O8 Y  [6 W; J( G终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    & p, N' P0 H2 Z4 e& V9 Q( o0 k8 y; _+ C* F- W

    4 T) i0 y9 z+ W( Jint GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度# A7 O- E- W; Y! T) t4 T( V
        next[1] = 0;
    2 i! Q9 t, C8 E0 I$ f    int i = 1,j = 0;% V6 _( K7 h$ b1 g$ ]! ]
        while(i<=cLen){
    : ]; {7 P2 ^* a: x) `        if(j==0||ch==ch[j]) next[++i] = ++j;
    % W) \: O# N; p0 g# x% c        else j = next[j];+ v8 i# H7 `- F# W  K) s* S& g6 `
        }
    0 g. F2 s" ^8 W2 }, `: K: D}
    * a# M2 Z( j, p* D' Z& V& E
    ; ~5 ]' S/ {# g. j% t还是先由一般再推优化:
    , C* u( v& n2 e" l9 o直接求next[j+1](至于为什么是j+1,是为了和下面的对应)) {3 A; ]9 h1 h8 ]& k7 [" `! n
    根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。1 l1 n, B  A; h4 E8 ^8 C% s. `
    不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:
    ; v# M' P. V3 o& N7 Lif(P1…Pj-1 == P2…Pj) => next[j+1]=j. S3 Y" R: F& E& N
    else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1
    ( Y, j! X4 @- d0 ^- F2 H: relse if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-22 T( }5 C) n2 ~3 ~8 b1 h* r

    1 @% t1 F- A+ A9 n6 g4 k
    % s9 k& _; U$ X5 {- M# [' r9 T2 D
    % {; J) C% }* B! Yelse if(P1P2 == Pj-1Pj) => next[j+1]=38 W6 @1 M! T1 Q/ d8 r
    else if(P1 == Pj-1) => next[j+1]=2
    1 C7 \  s, o, T8 zelse if(P1 != Pj-1) => next[j+1]=1
    4 E% }6 |6 w6 m) a每次前去尾1个,后掐头1个,直至得到next[j+1]
    0 v7 T" D4 ^6 n) q7 Y5 y7 c$ o; Z( X# l" [
    + E! P# E3 S# V6 R) M
    再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    4 W& v7 `" ?) H0 J: ?但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,# p; T& r+ B9 e4 ~/ ?3 p9 Y
    ①next[j+1]的可能的最大值是多少(即从哪开始验证)
    3 D$ ^- ?5 c+ N* }7 N②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)
    3 b% ?, \( O8 y. F看一下的分析:4 w/ y/ b6 [, P' G& n* {4 X
    : I, |# b5 {$ U/ ~  V( I- n
    & f7 }" H  @! A0 X1 C
    1、next[j+1]的最大值为next[j]+1。
    * p5 q8 U! m# i, \% g因为:9 m) ]2 @2 v4 z$ `/ ^# w
    假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。
    2 G* @4 F5 S- n& q" e# J如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1
    % G# x0 ^% E, y5 m2 e1 `8 P  n2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]5 z7 a# o$ P" ^5 ^
    这里不好解释,直接看下面的流程分析及图解% W+ w$ x/ n! d6 p4 t8 R
    2 p. o( `5 [( v" U7 b. h

    , S, O" d- s, F# [开——始——划——重——点!
    " q5 X$ ~7 T$ q4 F; c: }6 ^8 t从头走一遍流程  P8 e4 F2 c, [# J6 |
    ①求next[j+1],设值为m/ {' s( b" N5 d% c8 C
    ②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1" R0 W+ j$ k: ]# Y/ p  c
    ③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则$ l5 d' Y9 h( i& m
    ④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1
    . T  g" _8 a/ z6 i⑤第二第三步联合得到:
    2 ~. r+ }" X! W+ nP1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合4 a! }3 u5 y1 T3 Z
    ⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推( O& g  ~" R8 a: m+ s3 u

    : `! r! }& B5 |# {; c* w) _

    9 I& j2 H3 d' e6 b  N2 h上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
    % J4 ^) i' J7 {" |0 ?0 J1、要求next[k+1] 其中k+1=17
    " W; H4 ]% X- A! i9 C5 }
    999.png
    9 x# F, ^# z; _) \& W9 V" X. m

    7 l9 }* s' t. H2、已知next[16]=8,则元素有以下关系:- v8 C$ f0 M. w( K8 n. I, B2 H( G
    10.png
    4 V$ D5 z" l! y0 {) H- z' X% @

    9 x& F5 D- P; t2 ^. F3、如果P8=P16,则明显next[17]=8+1=9# E/ u& \! E+ W
    4、如果不相等,又若next[8]=4,则有以下关系
    3 P7 f$ T% l% Q1 N
    * |. a8 P6 p9 u3 V
    11.png
    . ~9 r) F) @! W& W3 G  m又加上2的条件知: U5 T; E: p0 M  G9 q$ O. _( j

    2 K4 R" n! F" ~1 N8 P$ a  N
    12.png $ ~: k) U! `+ j6 i7 n/ M) p
    主要是为了证明:
    - D7 w7 M' l3 V; p0 ^3 e
    13.png 1 i2 y2 `# s( w: m8 D6 i
    ; o' v1 f) K" D( M7 x! e& Z
    5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推& L+ B4 ?, d2 s( y2 T& P' Q
    6、若next[4]=2,则有以下关系
    ' E, w, C+ U# C+ x( \: Z5 D1 W. Q
    14.png 6 O) O, V) i! d1 G' ?1 \- e
    / C% d5 H8 k3 m/ T
    7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!
    1 I5 W1 c6 E: a& Y8 s" K————————————————% g6 Z2 y& \& a+ l/ y/ S4 G' i5 _1 K
    版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。3 I( k" y5 [; E. Z
    原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411& U9 A% U8 Y4 _

    ) ]9 D- I0 h! z. a# c, D, o4 \
    0 D* [1 ~# u' J) c' o+ u

    9 U, M1 R( @0 ?
    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-5-28 10:50 , Processed in 0.443578 second(s), 53 queries .

    回顶部