QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2935|回复: 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值算法
    ' f0 U2 l- f7 o" Z, j7 K% |# B7 C- o+ y( ]! Q
    大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。+ P: N& J# s8 P- U" W' K. r: g

    # l/ o/ b( z3 W2 c+ z4 x. ?
    2 J7 M4 a- S, _) ]" z, J3 R
    本文的逻辑顺序为
    8 Y% E$ H8 M/ h* `0 Z; Z  e. ?1、最基本的朴素算法2 N* ?! T- Q8 H" B- ?; Q
    2、优化的KMP算法3 g( v7 K2 J8 n# y8 p
    3、应算法需要定义的next值' {9 B  t/ a6 v* p1 J) @
    4、手动写出较短串的next值的方法
    % l' J* }4 W3 d! y5、最难理解的、足足有5行的代码的求next值的算法
    " C: g/ U; }( l. t" G& g/ z所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…3 [5 z" {; L3 b2 L% E
    : x/ d1 p+ {  A* ]0 j. Z+ I
    0 b! H, g! j4 z5 U+ [; w
    一、问题描述& `% v: e8 {0 S% t0 |; x
    给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。3 c5 u, f% U( g8 Z/ ?1 y) {( F$ i
    3 E* K& X! M( H3 z9 j* ]

    1 v) t# k6 m$ n' Q% g! p; m二、朴素算法
    & }* y1 D9 C  E! U1 ~: ^最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:
    + L( q, @: |5 G+ p" H9 S# M0 Y6 J 1111.gif " ]8 d& ]3 S9 h: _" [" R  B) O

      b# X) C: l! E! ?
    3 y3 _) K$ J5 Z2 a$ `1 K. w4 y

    , |# w3 B% o2 K这个算法简单,不多说,附上代码, V/ n/ \2 N" M$ h5 G0 P/ }) ~0 ?
    # h; p1 U8 z' {7 D: M; K0 C
    9 P3 ?) W6 M5 M% J. _
    #include<stdio.h>
    ' I6 C! d" D+ Q! ?' {' F2 d0 Dint Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数. r0 A& f- u/ d) V( W
        if(sLen<pLen)return 0;* s3 X" N0 }+ s
        int i = 1,j = 1;9 M0 r: F7 z  h9 `" P( _
        while(i<=sLen && j<=pLen){
    6 c% c$ i5 D" X        if(s==p[j]){i++;j++;}& l" b2 |* H4 S  ~: y( `0 ?
            else{
    4 t7 s+ ^' n1 H4 Y# ^4 T& k            i = i-j+2;; q$ K0 T# ^$ A0 B5 d
                j = 1;4 e4 K8 n- o# k. K9 G
            }
    5 t4 I' ?( S- y  `; ?- {    }  W7 A: K( j1 Y! o* O
        if(j>pLen) return i-pLen;5 y: `! q2 u! ]  `
        return 0;
    / E! U& f( z- \! ?" a$ M}
    9 c% v- e6 p* _void main(){
    5 D2 D. i4 d- u1 r. T    char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存
    ( M  ~6 K3 C( ?' Z2 \1 E+ m5 J3 s    char p[]={' ','a','b','a','a','b','c','a','c'};- `  P1 U8 v7 s
        int sLen = sizeof(s)/sizeof(char)-1;
    8 d. W0 J# m6 m. ~( ~    int pLen = sizeof(p)/sizeof(char)-1;
    ; Q6 p) x5 G' Z, j/ O% b, v    printf("%d",Index_1(s,sLen,p,pLen));
    4 S, B- `. ~* u& v2 ?! F}
    0 k" M; t: k( U" l1
    + W7 y! p1 O/ W3 F25 z8 B( p, t( p8 Q
    3
    5 j4 h8 x" ~8 I$ X% q4
    ( Y$ Y$ V6 o/ Z5 |5
    - T  i- v! l8 Z+ {9 m; A6$ L7 E: B0 P0 U6 y" A
    7
    ; r# _7 ~' u. z9 b$ i& E/ S8
    - Q# D$ {; J: w. P2 Y  d" ^9
    # Q7 g/ N3 A1 _+ s; P! ]103 v6 x% a5 [! g% ?  X
    11
    : I. d' c1 o1 \12
    ' M# X* V2 V# z- l13
    - b' z9 e) r$ W( Y8 Y146 Z) }8 C4 C; Q7 m; V
    15
    : B7 _* \, `% w' ~; P8 H162 l( R" c* D( d1 p* [+ o6 x3 c
    17
    7 }# E8 G" ~3 W3 l+ d; \) [18
    . |3 D/ b9 f1 Z  j" b4 b# @19, i9 P: ?. |! l* Z4 U+ D
    20" [9 [, k4 |6 ~
    21
    + [, B9 |$ n' j* ]( k* V1 g三、改进的算法——KMP算法
    " d% d8 L# v0 D* G9 P5 A朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。
    ) K! ^" ~2 v* O% ~; d一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
    9 h7 t- W! {  w: ^朴素算法中,P的第j位失配,默认的把P串后移一位。/ j8 `2 Y# I* Q3 u5 K- z
    但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:
    5 K/ y$ ?: {9 m, L* D8 _% i+ V5 w1 k: S* F+ q* _- z
    222.gif
    ( }+ Y9 Q- C4 S8 z% u! Y6 p
    - a/ l1 {* u5 r/ `
    3 K. J: W* W$ D1 t
    这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。/ w" k1 {& e( o4 b$ I

    , O, N' i- o8 h8 r0 m

    # U4 N" l, c  |% L, Q! t! J5 d8 N相比朴素算法:
    & d: p8 l$ ]* C  x5 v& Y# b朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)
    9 m8 t6 h- Y3 g6 u* }9 }- dKMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高' W& w2 _* K! j
    ; Y/ }+ _& i6 ?5 h5 t6 t6 V* @1 b
    " Y- _) m; G! Z( [
    而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)& N5 b* x2 J$ o; p. o3 ^, b  X' ^
    % e7 u) ~& y' k3 L: v

    + o' K- {& g2 l6 Q2 ^, X1 d! }开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!): E4 w: q" V0 |4 ?) T. w( L. ^

    $ M& k6 z" \2 U4 U) V: U! g6 M, G

    3 t8 @0 q  R" ]4 A比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的- z5 j, e2 |* ^( D) @2 I5 Z
    333.png
    $ u3 X9 o: k9 s3 }5 p) [9 r% q6 A% h6 f3 d/ G* B3 `

    4 U& b- Y* ]* @7 W  g/ Z假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”" j5 w/ S/ @, \/ z- x; Q  _

    $ y/ ~& Z3 [/ N$ m
    4444.png 4 k+ @6 v" O0 [  X) e

    6 e% t# R& x6 R% \1 C

      x9 a1 i$ n/ P0 ~那么可以推出,P1…Pk-1与Si…Si+j-2) F; F4 N% P# e5 h) s

    , {: O/ D; J- ?
    555.png
    4 ?# E) t* a' A( ^. _/ Y3 O
    . {+ R$ E# P- g& h( C  D7 f" G

    * @) Y% v! s. b' k, r) F显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:
    " P5 C' c! r, P# B! m- m9 m9 o9 }( G- p4 u5 w- M6 T& t9 P
    666.png
    ) Q# j3 A+ n8 s8 J# r8 D8 T# J
    & \3 c; |0 Q- t2 K4 X* {

    1 d) e! `- r7 G. u: Y% H9 ^为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。
    2 t0 D" I% U$ X% D
    - N* L: q* e) e0 X$ b5 H
    * I5 c0 `; L" `/ m
    最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。, D7 R* J* ~5 h: k4 \1 I3 ^
    $ s, i0 s  S: M  q) }6 a) D
    * I2 Z! s6 g+ x4 h$ T
    四、手动写出一个串的next值+ T+ p6 p3 I9 G; b
    我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:) M# i% C! i$ }& J$ h: }9 [

    ! K) v+ h  ^6 e8 s9 u/ l4 ]- s
    7777.gif : a3 @* |1 _: Y9 v
    这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。
    8 w. y" ~# d$ E: o4 w- q) ?
    * ~6 [7 q( a' w- R) K, x

    " e9 e2 u! E8 l通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:, ^8 U7 b8 F, Y( m- `

    : Y$ O3 i. O. w0 s

    9 z% V+ [' E5 l0 r1 g
    8888.png   T! f; `7 a+ `7 v1 I5 L
    / L! H+ l+ m7 Y# V; e2 V3 a3 m
    五、求next的算法
    ' X8 T  L1 W. S. g" N6 ^  ?终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    * F# ]4 e: u8 z5 q0 M3 `! `. l1 a2 w

    + e( f9 s+ O3 C8 z8 T# F1 _8 \int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度
    2 d" `  e, e5 }) A    next[1] = 0;
    1 d3 z% s' L' ]4 ^  O9 p  l    int i = 1,j = 0;5 Z6 C; L7 S+ r
        while(i<=cLen){
      ^/ D6 U6 ^  N        if(j==0||ch==ch[j]) next[++i] = ++j;" \& M5 q2 X$ x
            else j = next[j];8 O: S% ]% q: k
        }
    # w: ~7 r% \2 {- `' v2 J}
    * `4 Y/ O  Q, b( j# ?, ]% N" l" [1 t: b: {1 b) r6 j0 b8 [
    还是先由一般再推优化:
    6 T  z9 W% a/ S- g. h! u直接求next[j+1](至于为什么是j+1,是为了和下面的对应)
    / |3 h6 Y% }$ ?; a根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。- n4 r) P$ L! o% W
    不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:8 o' H. E8 g& }7 a& g
    if(P1…Pj-1 == P2…Pj) => next[j+1]=j. k5 j* p0 t) N( \5 t6 {2 `) b
    else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1
    5 X& w1 z2 H  @3 celse if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2
    $ J' ~* N: J  b' C( R% n8 S/ _; t* i# i

    8 K( E* C( T4 V4 F+ f; O/ Q1 R9 z3 ^4 A4 y: w
    else if(P1P2 == Pj-1Pj) => next[j+1]=3" v' A' T% E$ ]8 o
    else if(P1 == Pj-1) => next[j+1]=2
    4 z+ b6 b4 z6 I; S5 i' {else if(P1 != Pj-1) => next[j+1]=1
    9 N1 [  z8 Q  ~8 ]! |  r. y" ]每次前去尾1个,后掐头1个,直至得到next[j+1]
    . e" B9 G3 U! y" N+ m6 [# e" W
    ! \# ]0 w7 ^& E% y8 P4 k

    3 G8 ]; R7 R4 z; k( F; W+ K再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    * Y1 h+ q, t& `* D但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,7 S1 B( j5 v2 ]7 R! w
    ①next[j+1]的可能的最大值是多少(即从哪开始验证)7 f$ F8 [$ R. N# C
    ②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)- B3 P/ _' u" u' N
    看一下的分析:2 z6 U% R* U& J% O- A" [
    ; x+ I9 ]2 g4 E" P: x4 ~0 `) ?
    7 Z* X, m4 W+ M
    1、next[j+1]的最大值为next[j]+1。* w- a2 d7 G5 h  S. N; I% q9 x
    因为:/ |8 p) q+ B; X' P' H
    假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。
    3 j$ D! Y$ K5 j* ~) O如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1
    6 ^4 S, E# j# p2 _" |2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]
    : q& i5 b2 Y9 b9 K( t* ^1 x* A这里不好解释,直接看下面的流程分析及图解3 Z6 X  p; L( F2 g) a
    0 Z4 W( }6 j$ k  I5 \

    + E% B9 k; Y- n+ `5 y开——始——划——重——点!
    * O& j2 g9 P5 p& a/ `从头走一遍流程
    : \' I" q" F7 O. {/ l①求next[j+1],设值为m3 r* o- u) t4 [1 L+ S0 `
    ②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-15 T7 o9 U4 ^7 q$ {  E
    ③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则
    5 x' m+ @  V- i4 L④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1# d% `! O3 E, l# k  M
    ⑤第二第三步联合得到:- |& y9 k! r9 M/ I3 K/ f0 Y2 R
    P1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合
    / l. B, z( }) T8 u* H) v& b⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推: P  N! y% Q. d! R7 u6 K" H
    : v& A6 y* P5 x7 N. _& d
    5 _+ `4 {% n+ ~2 |2 v% @
    上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
    / Q7 N+ n& {0 L( S1、要求next[k+1] 其中k+1=17
    + h9 ~( X+ M( M7 Y/ K
    999.png
    $ j1 {1 T" @+ o" u' p
    . a% z# y# u3 i9 h
    2、已知next[16]=8,则元素有以下关系:4 c' j$ e1 A9 a+ q. t; b
    10.png
    $ L& H2 u- p& T4 m/ }' k, }7 t
    , k0 Q4 G  ]/ B, l2 a2 W8 d
    3、如果P8=P16,则明显next[17]=8+1=9
    2 K; t$ E# ^$ G# P/ c3 z4 p) y4、如果不相等,又若next[8]=4,则有以下关系
    5 l+ ?9 {6 E6 ?. z
    6 h9 F. ^; l7 `4 J& b8 a" L
    11.png 9 A1 X7 Y# b3 T  T0 V3 I6 i& g
    又加上2的条件知; X$ y5 a! q0 `" \3 O5 m

    + Q- A; {# e4 M6 z5 m9 v
    12.png
    ! Y6 ^" z% b; ^, ~主要是为了证明:; [9 e1 e/ D/ ~# y( _. S
    13.png   g% k0 P2 E2 m- a3 z

    8 h9 l- ~# M" P0 s' t# u5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推$ b3 u' }9 l' ~( r
    6、若next[4]=2,则有以下关系
    & ]6 U2 L) f5 P7 J
    14.png
    ( m2 l3 |( `1 p) j6 q9 o3 u" l

    % p7 ~/ J) L2 u1 F3 V7 O7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!) c' v/ h$ z8 k, b- y# U: n
    ————————————————9 }/ C1 e- t# u* v0 |- H# B0 X
    版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    # D+ Q% `; G6 K原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411
    . ]6 i5 Z8 ?8 K) L* c8 M: l
    7 A8 K* A# [* [2 P- Y. z: l" C
    , d8 ^, o) m, l0 ^! a

    ( }! N8 g' Z5 f9 _& j" U8 g3 s9 \
    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 02:14 , Processed in 0.352232 second(s), 54 queries .

    回顶部