QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2901|回复: 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值算法
    % \6 {; O9 I4 `# `( i& b2 H# X% L5 v) P; D
    大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。
    6 O- P+ ~" K% O- ?" l  R
    1 q. }$ z2 x: s% z

    6 s) [% _3 \+ C$ m7 P2 L本文的逻辑顺序为) k  U, \9 O- o# l' k0 \
    1、最基本的朴素算法. x- o) u- {6 U2 r5 h# Z
    2、优化的KMP算法* }; d8 X, Z& d, S
    3、应算法需要定义的next值) k6 r2 Q) q' c: ~. J7 d- |- T0 Y
    4、手动写出较短串的next值的方法
    4 G4 i* E- I  M! [8 {2 y1 U' Z5、最难理解的、足足有5行的代码的求next值的算法2 C' e8 M7 W& R9 a& P/ e$ q- `1 Q6 N
    所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…6 B) Q  w) R* h

    ( p) D3 C( L0 m9 p! A0 {; i

    3 K# ?$ g0 }: g' H9 t8 o一、问题描述
    4 y+ u& L/ p' z* Q- o3 B给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。
    , b+ E2 q9 g' u/ S( k3 u% Q. ~" f$ E# C5 Q
    - L- S7 }' l" _- x" K; J
    二、朴素算法( X8 b( J4 M7 I" Z: v
    最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:; ?/ T; A6 ?' j2 Z
    1111.gif - N' ~7 y+ x$ p- H$ N/ U  f8 h
    5 }: q( I/ t. b; M! `% Z
    3 G0 e5 `0 _3 i5 ^* Q' b- y: [- |
    * C& O3 M8 [# @. R" `1 V) S
    这个算法简单,不多说,附上代码
    ( l. M7 t9 X9 c3 \6 X
    ( {9 |4 b0 ]; Q& j
    1 M6 Q4 H9 }; U) b. a! I* x2 B
    #include<stdio.h>' T- X+ W& h$ s/ M% f( n4 }
    int Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数
    . |' @1 o( S; M  V    if(sLen<pLen)return 0;
    % p/ A8 g. T2 \" ?$ B    int i = 1,j = 1;
    ' a1 U1 j3 S, _2 E; C    while(i<=sLen && j<=pLen){
    * m) j; Q& k1 x  L6 @! g; r2 \* R        if(s==p[j]){i++;j++;}
    2 G% a, ^( ]# M4 H# X        else{9 A! R6 |1 A4 k+ c4 \8 W
                i = i-j+2;
    1 a' X& I3 o6 T* A' {            j = 1;+ Y: g+ D0 n* W, ]5 m& g. D( K
            }7 v* J' T6 z- |8 m: u" a2 s
        }
    / [' v) `6 d. B) Y% y    if(j>pLen) return i-pLen;
    / i) }- T5 F& L9 B; M5 B' c5 g    return 0;5 Y- \, h7 c6 b  c1 m
    }% j+ s. [6 @. y; ^, o& ^
    void main(){
    , \* b$ f% X5 O7 J4 y2 R9 q    char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存
    ) }' j/ A; q7 N& U& M8 r7 D8 n    char p[]={' ','a','b','a','a','b','c','a','c'};
    ; g# C% M, E% t( A# Q$ k    int sLen = sizeof(s)/sizeof(char)-1;7 c* i/ j- V; G1 o, P
        int pLen = sizeof(p)/sizeof(char)-1;' J# D( J8 \0 O# y/ t  r
        printf("%d",Index_1(s,sLen,p,pLen));; f2 C6 A& A2 l" u" T
    }
    * A4 d2 Q0 J9 Y! R  ~1
    * K! U$ T! e. v' u  ^21 |, N9 P2 D$ c7 W( |# E3 T  x
    3
    3 g1 d: L) Y5 D6 ~: \7 x4+ u  b6 T4 ^+ q. z/ _- ?' w
    53 {. q- i/ b$ ~; X
    6! _7 n/ |1 L/ i4 J* d! z
    7
    + p( Z0 t9 j" T83 U: q, U! z5 T, j4 |
    9
    4 Y. f0 ?7 T0 x2 I% X10  f0 o  v: S# F6 T. u: X& |
    11
    ' c1 X- x) c: {/ G, n12  E- l' `: {8 A! N: ~1 A3 I3 J& P4 p
    135 @: T- x9 g+ K1 b
    14
    + F3 M4 ~. d) d& i6 d159 Q; \, S) k: h3 M1 M
    16" r! l' \' Y# q) }4 G) u
    17
    & t+ J$ f7 H6 p+ E18
      w( |. h- {; m  W; I; U: E197 j+ q: y1 K% E% L! c8 |; D% H6 `# ~
    20' f: K. x9 R' K/ U3 _4 W
    21
    5 Y+ Q7 g# _3 {' V三、改进的算法——KMP算法4 U$ E( _2 y# p+ u
    朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。% \& r! A1 ^6 ?6 [$ Q
    一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。9 U" T! B, u( A& l/ L5 P5 f/ u( k
    朴素算法中,P的第j位失配,默认的把P串后移一位。
    - g3 |0 [- ]) u; T- R# f但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:) n* Q& B, g/ C$ M8 r
    , w& ^% Q7 ~; c4 T- ~
    222.gif
    & s2 H+ u& H0 n6 v3 P/ O( L) L; Z! b$ V# }/ E

    6 z$ o' r, W7 Y这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。6 Z8 Z) n3 s; d' ?" e5 Y0 N+ i9 R

    0 ^  e! i" f+ ]- A% ]/ C

    1 Q, ^! A) i; g- ]' F) D. O相比朴素算法:
    3 I  V& t. B& C* z9 k* x' E3 F  i朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)
    # j0 Y7 g( s* d9 C  zKMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
    3 y  g' p: Q  a
    ; `8 {' l5 Q& B* y0 a

    * `, J8 l* n& r9 g3 e而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)
    : z. |. q1 [/ v+ V/ V/ e6 F9 D% M5 w8 }7 o7 U5 P6 F
    6 R/ U; ?  V: W4 a( b# l
    开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)6 p, e: f" L- }5 x4 r

    6 ?* O: K* D6 h, \. l3 g  j

    5 _  E$ ]3 k7 z8 N& ?0 }' H比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的6 {2 w# G0 B9 k
    333.png
    % n2 B( z8 d2 I* q- O
    . q" ]8 M0 Y5 e6 z. k6 ]( J
    " w1 Z0 n# i- B0 }, D+ `
    假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”
    # }) v& ^8 C& h1 b/ {+ x/ Z% l6 [; t
    4444.png / [& X- ~5 t% E

    2 L6 e# z5 t2 v) Y. ~9 R

    ! E, A" [; z2 K9 V- q1 {% C6 E那么可以推出,P1…Pk-1与Si…Si+j-2* ?: `! @8 v' W0 u

    1 c) `$ k- P' E1 S( S' F" l' v/ j
    555.png 7 G1 G8 w2 X+ I/ `* U. Z# c

    8 \1 v3 L; c. Y& B

    + y( D' J, h4 e4 N# q' }显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:* }8 G. D; E$ s2 Q# v7 N: \% W% s! e$ r
    ( m2 K# E1 ?4 j. [) J$ L
    666.png ( F2 m1 [: z* a2 H: F
    / J+ ?! t& \3 P( }# k2 e; s% E# i

    + V5 B/ h/ w5 K4 {. _+ i0 \5 b+ }- R为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。& D9 i" e' L- b
    6 s' D4 t' w$ L/ K
      G9 d8 S0 u! Q
    最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。. g4 u0 g. E, K" j
    % l  b9 D0 q: c8 B

    9 R6 B3 i0 G6 R) g, t0 B四、手动写出一个串的next值
    8 @: g4 W; H0 \* v2 ^, T我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:
    ; `7 v) D0 W7 h' z3 ^* f0 V
    1 L2 M7 r. o  r0 W
    7777.gif
    , B+ A6 E9 k- G4 ?7 n# |& v这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。
    * q1 Z0 h1 i. p; v
    ) z4 K, a, c4 C7 G
    & H6 U+ M* o: N' R
    通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:
    : {7 X0 a" O7 \& |
    - F2 U2 b0 f- v2 o( K; L/ `, ?

    $ G: h8 c8 i2 B/ R" j' s, I& u: _
    8888.png % g" K2 u5 k) N0 W

    9 J6 r, u. _: c1 D" n+ Z0 J9 K; C五、求next的算法4 Y4 q5 r* b0 w
    终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。) p6 x8 F& ]! s
    0 N. e% P4 ?$ _/ f# i! S, S& G
    4 Z2 G3 Q0 u- Q, B. R  n
    int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度- D4 A5 C$ E- p/ ]/ h: Y& r
        next[1] = 0;' S1 c+ @( m1 E( s
        int i = 1,j = 0;0 y2 f8 _5 e7 x. E. G1 u; j
        while(i<=cLen){0 d) s2 {* X( ^$ b8 P) E* Z
            if(j==0||ch==ch[j]) next[++i] = ++j;& L# h* T6 @! n+ H/ a3 P( A8 w
            else j = next[j];0 _/ K- K9 h- P1 R- q/ z2 S
        }1 M/ Z3 c0 j4 V
    }* t2 e; {& b! c* D- f

    ) R- E0 @5 A  e" i! }还是先由一般再推优化:! S% s+ a; @: d; W7 v
    直接求next[j+1](至于为什么是j+1,是为了和下面的对应)
    0 Q9 K7 f0 e8 h! l$ t& Z" n根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。) _, z& j- ^* w, u1 |: M
    不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:$ Z/ _5 w/ V7 n) C' S  k! A7 P
    if(P1…Pj-1 == P2…Pj) => next[j+1]=j; e0 C+ E3 S7 j
    else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1
    8 ?5 Z$ R) f; u2 pelse if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-28 j+ c: x+ T8 x2 ~: C, j
    8 N  C) k# p- ~8 d

      \! n9 v& b: a& ]; n) J  \- ^
      W0 B. b% c5 h+ }9 j& C2 U1 |3 Celse if(P1P2 == Pj-1Pj) => next[j+1]=35 d* C( C5 `. H9 y+ l
    else if(P1 == Pj-1) => next[j+1]=26 @; V8 V2 w4 c. G
    else if(P1 != Pj-1) => next[j+1]=1& t" @9 T- z& g  ]
    每次前去尾1个,后掐头1个,直至得到next[j+1]
    " d4 e2 T3 Y+ q% M( K4 O( H' H& J# p" Q9 v
    ) L1 ~6 m% r+ w; ?( L& X
    再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    ; E  d9 l8 V( R6 i5 R$ R0 R$ \& a但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,
    * Y- T* C0 n" E& j4 k% E( ?. a①next[j+1]的可能的最大值是多少(即从哪开始验证)/ A  f7 M& y' |1 ^) _
    ②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)
    % `" m0 M% C" y) R; G+ K7 p看一下的分析:# s" C! I; j) A0 Z5 }

    6 z! S! T" c# d* _& i! W$ @# _6 t

    ) _$ x  ?  m* }" X1、next[j+1]的最大值为next[j]+1。( g& l6 {! |/ x& O* R+ D
    因为:, ?" k5 s/ C$ e6 `  y( |: K
    假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。. s$ j5 n% X! g: ?! [. S. r
    如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1
    ; E: R. F& \) M: T" C6 Z2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]7 ~! f: J. I/ `8 ]; T, Y3 k
    这里不好解释,直接看下面的流程分析及图解
    6 z3 z5 b2 C- f: m7 n6 d% ~. Y, }! r3 `- Y" j' v, {
    4 k7 ~1 k; M) V; Z' k. L% p2 s
    开——始——划——重——点!
    ! V( |2 z$ T7 a从头走一遍流程1 V; H8 t! a1 b0 {% F5 V! l- J
    ①求next[j+1],设值为m
    8 b: }0 f7 [6 r, G8 A, j②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-17 `* }& @- s/ @
    ③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则; g( T- H  y6 d6 E/ A
    ④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1$ t2 d7 F5 H. r* E
    ⑤第二第三步联合得到:% f( \5 N2 S; R+ x
    P1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合
    : {/ S3 d, S0 s1 F0 v⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
    6 u# h) Q! p, ~6 S6 b4 V4 ~5 H1 I

    2 T6 {7 M+ x5 I4 k' j, N上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
    6 p, Q+ P2 ]1 a% \" B1、要求next[k+1] 其中k+1=17
    ( ~4 W% a: K3 \% n1 z
    999.png * z/ s6 P& \* f& l  z% [+ L
    9 J  N/ r/ _! K, I8 k; ]5 @7 x
    2、已知next[16]=8,则元素有以下关系:  a+ {- h+ I; O. x2 @7 A$ X
    10.png
    3 |& y& v6 ]( {5 [, |

    9 t$ }2 o4 i9 q: O3、如果P8=P16,则明显next[17]=8+1=9* E' ^$ m. p2 s7 ?
    4、如果不相等,又若next[8]=4,则有以下关系
    8 p2 H1 @. K1 Y  [, y, j1 b- B; c8 ^* U& n$ p
    11.png
    : r1 G0 ^% Y$ r; }. F- V又加上2的条件知
    ; D/ @7 m0 n1 f) `  o# V( q" n( y
    12.png 0 F* ^& B. V& S; m' i
    主要是为了证明:
    $ o$ y/ c( ~/ ~$ N# p
    13.png
    7 W5 w2 ?4 C" E6 I2 m9 _$ {0 I% _( E

    # B  r: p* P7 {. |2 f8 j" B5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推
    2 {, G: t% ?) ~# J! z6、若next[4]=2,则有以下关系' W+ _8 M  {8 h2 E0 p
    14.png 6 _; n$ ]8 ~; i' Z# q
      Y" c8 I1 R9 N
    7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!; M3 D1 A& r$ r) R; v
    ————————————————' Z5 P3 V7 {6 E8 v0 ~: m
    版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    ' D" H" r2 i- |原文链接:https://blog.csdn.net/qq_37969433/article/details/829474117 ^0 C: x3 b" B- W# M: r

    ' a& S6 o9 C3 g8 M
    / Y, X) |8 G8 D/ e5 K: A

    : ?) h7 H) X" T
    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 18:34 , Processed in 0.451419 second(s), 54 queries .

    回顶部