QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2894|回复: 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值算法
    - q" }6 q9 s" @! \3 h  b4 a* Y7 ^3 R9 R! x: _, M
    大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。
    4 e! K, w0 e# v1 x) J/ `" b6 Z" Y3 m& z; A

    2 L" u  i9 I" r9 Y# O本文的逻辑顺序为
    # l& O+ |5 S* |# ]1、最基本的朴素算法) C$ z1 `6 p$ W& H+ |: D- y5 ^
    2、优化的KMP算法
    1 Z  g0 W! a* V7 j6 |% A1 g3、应算法需要定义的next值
    5 V9 f9 N1 n9 ~# [8 f( f4、手动写出较短串的next值的方法
    ; E) M% E/ B7 K, r. j) Z$ W5、最难理解的、足足有5行的代码的求next值的算法
    - a$ o( d/ l- O, x8 y* L所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…
    + y7 A! C5 c7 N6 a6 }/ ~& }+ I* ^# f# k/ i7 B* N5 i% Q

    ) u. c2 H! \7 a" f- t2 B9 c' U一、问题描述3 T, W# m) p% P
    给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。
    6 S3 m6 V' D* z% W* I& s* V( n
    3 t* ?, E  r  G. ]  z

    ) K2 F# Q* ?8 @' l: f二、朴素算法
    3 @' _/ S! s2 \; A& e( k/ [7 C最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:
    , \3 Z" y! M) a9 V, F( P' M- Z) k 1111.gif
    3 T  h, v6 Z% @  t

    - e1 z% k& ?4 Y; U( i1 m5 F! f; q" Y0 E; A) x: g7 F

    0 W0 O" w' l9 C2 n5 p0 a这个算法简单,不多说,附上代码
    0 z  Q3 e, ?3 l
    4 k1 H& r$ t- `6 r

    ) u9 I. g. X5 u  Y. Y3 ~1 o# g9 N#include<stdio.h>! b0 {" A* ~2 u" f5 S* l4 s
    int Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数% g, H0 _, \) P' V
        if(sLen<pLen)return 0;
      M! c1 G; {5 s$ i3 i/ j    int i = 1,j = 1;
    0 a- S% O- j% w  l3 X    while(i<=sLen && j<=pLen){! I, u% _  b+ _& ^7 O$ M
            if(s==p[j]){i++;j++;}7 o0 j  P/ v8 [8 A1 \% c$ W
            else{) C8 [1 A8 S: @, D1 e9 E- h
                i = i-j+2;& s3 \" a  F9 V* u
                j = 1;
    / n, h2 m! H! L+ F) y+ Y7 I# X        }
    : g* h! {9 F; m( k% {9 |1 O' Z" u    }
    0 P" W* [2 }+ x+ Z% C1 H, F. [    if(j>pLen) return i-pLen;
    ( i" k( T; a. J  P    return 0;/ }* ]9 p' `. K" c
    }& W% m; u' m& r6 j$ _
    void main(){# z) }. n- C& q$ \8 Y
        char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存- ?  r+ Q9 l! \& A' L+ X
        char p[]={' ','a','b','a','a','b','c','a','c'};
    ; \8 [4 l5 C$ N+ T# u    int sLen = sizeof(s)/sizeof(char)-1;, Y  P. f" X# x; Y
        int pLen = sizeof(p)/sizeof(char)-1;
    6 Y/ r8 x$ t5 [' @! S# w  B$ N    printf("%d",Index_1(s,sLen,p,pLen));7 M. C4 W8 L4 x9 ]$ \  K
    }4 j& u" u' d7 Q# Q
    14 V+ R0 m! R) |7 y: w
    2
    8 k" a6 `; H- Q& x! \& V32 A9 n& l0 T( g
    4
    4 i) S' {; C9 ?: |" X5) \: `- a6 v1 C1 S
    6+ e& o" |" [3 i
    7$ v" j9 Y; v/ t2 ?# v; x/ A
    80 ^  g  s6 ^4 G8 S; n- m: O. |9 w; Z' I
    9
    8 Y/ k' g8 W+ S! l  e0 a  s0 z104 M1 x7 ~3 y! C" i1 E
    11
    * X, t9 L, O' T2 U" u, u12- O; e$ F) s# E- ~! [* Q
    136 u( a) K0 g8 b+ \0 K6 q7 v2 ]
    14
    9 L9 v: M" N1 i) ~$ }15
    * r6 L/ g8 c: }, J$ K4 u5 s# {16" W0 R6 V7 o9 i% j
    17
    $ F5 Y# M( k5 t2 p. e( m; s6 Z18# r, K3 V# J3 m
    19
    4 ]+ I7 r* R& Z- y20. h( S! `- X4 P$ L* k, Y
    21; ?8 a9 W9 \( S
    三、改进的算法——KMP算法; b" r, @3 G, j6 D4 r  p
    朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。
    ; x- y- }$ Z; I/ H' E& Z# D$ P一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
    , _5 S+ x7 z. R  g朴素算法中,P的第j位失配,默认的把P串后移一位。; y9 @- g- I; p
    但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:( X- C# o. C  \2 J  U* O
    % J) U# Z& q8 M" ^9 ]# {
    222.gif ! t! H; e' K7 S7 ]
    7 H* e( d  t$ G9 z' f$ d$ t( @
    * k' d' V3 A1 K" y! @# Y* o3 i, t& S
    这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。, C; }# I5 w# O* d$ b( G
    % E! u# H, {. |. \& T

    . D# [! ^8 L. x, ~5 f: }相比朴素算法:) B! C; G+ \, F5 d
    朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)& B3 y. |& @- E- |) F& X9 q4 Z
    KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
    " h; ?. k2 u- t' e9 O
    $ l3 ]: C9 Q* z& |7 m3 f: A& a

    7 T: m% T2 x8 q, X而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)
    7 Z8 T2 W6 W( A/ |4 V: }' r% R: o" @2 Q

    % z8 h% e# z; F: v  }) x6 u开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)
    : k/ J( c2 E- P( M$ y1 F. F5 Q
    ; D) k1 ]1 J$ i

    ( u( I- Y8 C4 G; Q7 l* S比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的
    0 ~- \+ N( I+ z1 X- [3 e7 R6 ~5 i
    333.png
    2 z2 `$ |; @( Q) }
    + U4 m0 I  U; D9 c

    6 @$ ~# P! ?1 x( E8 b假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”3 n; B+ x' t, x& l

    % U2 ?7 q2 W# j2 w
    4444.png
      h& `, b, n" Y& ^
      C( }/ F: r4 t1 l* A

    4 H4 ^" Z! {: t5 t9 B$ N$ f那么可以推出,P1…Pk-1与Si…Si+j-26 d* v9 J! v8 x7 a
    7 }/ |+ o5 e2 g% w& F3 z
    555.png ! S$ O  s  h. _  U$ R, }3 \( n

    6 {. ?1 r3 i# V3 P* j" E+ E( o# k

    ! T- W# h! l! o' t3 }* @. d6 P. X显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:% o: D5 H5 a$ I: G
    8 H8 A. I4 ~, a! {, q
    666.png
    * Y% W. A! ~8 V4 R. c% j, Z# {& }3 P6 e7 w( @! F$ ^5 j5 S1 \

    5 j" P2 U: w& z9 R; O: u5 Q为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。
    ) d- N! s7 b4 J: a0 t$ y; l
    9 t. ~" J& o' T, t; C! _
    / U" N3 P% R% X, {& j- l6 T
    最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
    ( M( M% F7 D& R* X2 F* p2 E4 ^3 f9 o2 [' I. h# u$ b

    5 x$ `. c( Q, o- h8 @四、手动写出一个串的next值/ W  f, u8 I' J3 y+ W( o7 X
    我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:
    ! o5 S  o! L& O. f8 K7 e" R- H$ `0 C
    7777.gif 5 T$ X( H# l$ {
    这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。# P" W* q3 u8 g7 V/ o$ N5 r

    ( F. a( K2 J% _/ G$ p( c, L
    / e5 I! G' W) u5 r" {; u
    通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:) L5 l- h& Y& Y/ D/ o; D' c6 U) V, ?

    7 V! a4 e% K* H$ {% q, ?3 m3 o

    ) o6 T( k: _1 N$ U4 x7 |9 W
    8888.png ) @" C9 G1 H0 q
      A3 _. c( v- i
    五、求next的算法
    ( S( F/ _: y( ^+ y! r终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    ( r3 S5 V; e( y2 l; m
    - b: t: W& e- t3 ~- [
    5 i% ?0 e) j& j8 X4 K. Y
    int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度! t4 Q, r( |; @" ?7 O. U* x# m5 i) b" \
        next[1] = 0;
    ; W; \0 K1 x8 L* i2 h    int i = 1,j = 0;
    ; ~' {6 x# y1 z1 j5 a/ j    while(i<=cLen){9 [% M. k3 a, {1 U% u6 n
            if(j==0||ch==ch[j]) next[++i] = ++j;
    ! t/ p& N$ i, U4 W        else j = next[j];
    / L$ s1 u0 g, `- c  ]3 c+ F    }
    # K$ k( v& |) K  y}3 t6 H0 h3 Y9 E3 i6 O7 c; s

    ( t9 g2 h0 e2 m4 a$ K0 k还是先由一般再推优化:+ C3 y- V, }9 @
    直接求next[j+1](至于为什么是j+1,是为了和下面的对应)9 ?$ C8 v' p/ k
    根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。
    ! b4 f' `% K- ^; f1 ]不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:+ h0 a9 D" i) A: o/ L) r6 v4 f" d6 _
    if(P1…Pj-1 == P2…Pj) => next[j+1]=j. X7 O/ U, |+ U: ]) n
    else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-16 {! C! m/ J5 Y+ A/ e% ~2 y" z
    else if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2
    ; i9 d3 q9 ~$ b% n) |3 u- [
    # N5 m! ]6 c  X+ H$ e8 q2 I
    , m+ ^+ u9 N0 r+ E4 O$ x9 M2 x, O
    9 Y4 K, I: N# y$ ^* S: C' |1 helse if(P1P2 == Pj-1Pj) => next[j+1]=3; b, Q; B" @4 k: W4 b+ ~* k
    else if(P1 == Pj-1) => next[j+1]=2
    & @. l# ^- z* k( Uelse if(P1 != Pj-1) => next[j+1]=1- U3 Q* |" G$ w/ {1 i* s8 ?
    每次前去尾1个,后掐头1个,直至得到next[j+1]
    9 j# x2 _9 r8 ?& u( X- a
    * v, U0 J' t# b+ l; c

    7 s( m" B* o3 _; A! r$ s再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    % P' p5 ~- T& l但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,$ M7 D1 x- v! r! x
    ①next[j+1]的可能的最大值是多少(即从哪开始验证)
    2 G% N4 T0 k! g0 |: e! |  w% L- p②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)
    & V. k, [3 q1 G. A' c看一下的分析:
    # i* W: F( w6 j( t3 j% O  N
    ) C. F% m8 @$ O" H; n; [

    7 F9 l0 S& p( y2 S* |% B1、next[j+1]的最大值为next[j]+1。. a- k% J6 {/ z
    因为:
    ; c$ j/ R! ]; L; S0 B# e+ Q假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。
    * e$ q3 s! y# w, A4 e8 {: c9 c如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1
    2 d# q2 ?2 z9 m3 [2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]
    2 F5 m, Z" Z$ Q  c6 d这里不好解释,直接看下面的流程分析及图解6 ~5 ~" T# q) Y8 F- E' C+ O9 b
    & x3 q. `% C+ w0 g( @, m

    7 d4 P, g$ C9 S. Z8 a! G& j; v开——始——划——重——点!
    + l' Y+ ]; {. ~( Y( ?0 e从头走一遍流程
    2 p, O+ q( J; Q. E3 v. {①求next[j+1],设值为m; v6 |. S) a' G+ g7 I5 q* w  A9 g
    ②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-15 M8 h8 x* R1 i0 `
    ③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则" n. ~2 c- L% _" e$ Q. O) S  g
    ④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-18 U* i# G6 B( r7 z3 e' w; r
    ⑤第二第三步联合得到:
    / ~/ m* `- q# y" `4 PP1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合
    6 Y7 I3 y  P! ]⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
    3 R2 q5 Y( J1 c7 t+ K& [% K" r
    , w1 _% `) X& N9 G& m

    0 R. F) Q/ I' K, B) H, G2 |4 ]上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
    & B1 f5 O  \+ N9 X; p% }1、要求next[k+1] 其中k+1=17
    / p2 x0 {, v! s. e0 Q, r6 P/ c% k$ j! @
    999.png % C# k1 _0 ^) |
    ( A5 z$ M7 Y6 u! f4 x
    2、已知next[16]=8,则元素有以下关系:
    7 d7 Q/ N& ~2 e5 H) x* s
    10.png : x- x; C! s9 c, h9 ~! u

    0 [' N- B* L  i+ m3、如果P8=P16,则明显next[17]=8+1=9# t- P( u2 E3 j; {# O5 q6 Y
    4、如果不相等,又若next[8]=4,则有以下关系
    0 b' z5 ]) h3 t6 p/ `
    + B. k; {; A; |$ d; s5 Z! a2 l2 [6 f7 d
    11.png
    # y0 t/ q1 b" S( ]  ]1 X1 F( i又加上2的条件知* q0 c' ^3 c9 W% N% X+ v" r$ K7 }6 Z
    * @2 y1 j6 Z4 Y2 M- Q( o' p
    12.png
    ! N% j* P, A) d: U$ Y( B1 w主要是为了证明:# [3 M6 Z! M7 q0 f$ }4 ~0 i
    13.png
    ! T) b3 f7 a  }4 I: t
    % [0 W( _9 `1 _: L
    5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推
    ; [2 E) F+ p! G# c% C7 V6、若next[4]=2,则有以下关系
      v, V4 J' x/ O1 u" j
    14.png
    ( g9 @" u% E# h5 d: w) `

    ' N9 A) j( n1 ~8 a/ I2 W" g5 b7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!
    . U: X) {' Q6 [————————————————
    ) I6 A  z7 x% m8 x& {% Y* \0 ?版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    " o* P7 e5 d) y' l& M4 Y原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411
    ' U7 q- ?" Q% o* A$ f3 S
    ; n1 V- ~. n; R) ?- T( ~# K
    6 I1 c! J: h$ U% t9 B) W9 G  O6 H

    " N1 o8 F! n' ?+ }/ P) u! A) M
    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-26 12:31 , Processed in 0.373462 second(s), 54 queries .

    回顶部