QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2933|回复: 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值算法
    0 x  r. T+ i; t2 u4 I% k
    1 E& a$ V+ b& Z大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。% D* B- G8 v! z$ F  N7 |
    ( I$ w, P2 u4 |; ]- H- G6 l" [
    , L& M( S9 R, n$ Y' R
    本文的逻辑顺序为$ w- v$ D) _" w6 [" [, W0 Q5 O
    1、最基本的朴素算法
    . g- x1 {' F5 Y/ a- O" C2、优化的KMP算法
    " u- c& z- f( t: P3 o" m3、应算法需要定义的next值
    , @; h9 ]) L* M" x/ z$ t5 _4、手动写出较短串的next值的方法
    3 ^" }; B" U  q* T  q. r  ?+ U5、最难理解的、足足有5行的代码的求next值的算法+ N6 |( _$ W! Z0 |- o6 D$ V
    所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…
    8 ?8 l4 R  s' P9 e7 q* A; p; f; R* J/ [# E" p7 E, w8 x1 S1 {

    ; s! L+ ?4 x1 D: `一、问题描述) ~2 G: I5 u. }( W6 x0 F" }
    给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。
    " ?6 w8 r4 @( q6 r* P+ ~- Q
    8 q* l: f  @/ \0 e6 i8 L6 O
      S* E( C+ f4 J) q
    二、朴素算法; O, ~' e+ E1 {
    最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:
    3 ~9 V* @7 |$ a# T& |1 h 1111.gif / a- |# S, o4 x0 R1 V# n' ?

    / A, W) Z* |% h* L0 G' U" |$ Q2 l5 d- j/ }! l

    . P: e$ ~+ ^9 t5 X这个算法简单,不多说,附上代码3 S! Z4 M1 F7 _- P* J: f

    % R) M# c  D2 Y0 a2 ]. z
    + P6 L0 F$ t( g! p
    #include<stdio.h>! _) u) |7 s0 T* e- h7 n
    int Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数
    . T7 s+ F4 V& Z4 a7 U8 M; M5 p    if(sLen<pLen)return 0;
    ( W5 S& x& `8 [4 S4 U" L; b* y5 a7 A    int i = 1,j = 1;8 j* w+ F* `, ?0 p0 q! V% h
        while(i<=sLen && j<=pLen){
    5 S1 @% o0 ], t5 Q        if(s==p[j]){i++;j++;}
    7 j0 x7 o/ I$ t4 I* B; a, }. c/ Q: q        else{
    ( ~1 X9 A: x. R  O) u, k5 }" _            i = i-j+2;
    . D$ s" Z# `- a7 X% }/ ]0 M            j = 1;+ Z; I' b; v/ m7 d# D2 Q1 \7 e
            }
    + y6 B* L- w3 ~1 i    }
    1 X& E# y, b* P0 X; u  r, a    if(j>pLen) return i-pLen;
    3 W- f0 `1 H8 v. u$ g4 E9 ?    return 0;8 r+ ^3 z' k! p" H
    }& y$ i9 s& Q: r: e/ A' C
    void main(){2 q) J0 t( R3 @2 h
        char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存
    - F7 ~2 _9 |, t) C4 a    char p[]={' ','a','b','a','a','b','c','a','c'};% h* J& Y+ n& Z
        int sLen = sizeof(s)/sizeof(char)-1;
    * Q& H3 R2 |; W( @" Z( K' t    int pLen = sizeof(p)/sizeof(char)-1;
    8 C6 ~4 S' ]" M0 z# e; F    printf("%d",Index_1(s,sLen,p,pLen));# X# ~2 O( @# D, h. h; {
    }
    * `6 U- y8 g: d$ I# g* O14 H. D/ g7 C: U
    2
    & z4 n) q2 [$ H! j0 j' B" f; L, W30 f% l& |; z4 b% l2 H  {! `0 n
    47 i4 [' U+ V- F
    5; P9 h! y0 X- r6 t7 p
    68 G3 I- Z" h& ]$ @" G
    7
    ; L- k/ M- F, g" ^87 [$ M: A( S. _& a: a( z
    9
    . C$ l9 Z. ?6 p4 I10
    , w3 _5 x/ E+ B2 G# U  Y11, `! N, U# P9 L' x% a% u$ b6 L4 ?
    12
      _# \: y% M9 P5 d& e: [139 P' x$ C' M. A, M; O
    14
    2 R: T  L; X( v# f' m15
    ) U( O0 ~9 d! l6 i7 T/ m: U16; l# A0 n, N8 G  b/ P% D8 X4 W
    17' G+ L5 c; g5 q% s
    18+ u4 C. R# U9 Q* b- [9 z' ~4 u
    193 B+ b& {2 X; M% M1 }0 r8 t. M5 A
    20
    0 K) }0 T6 u7 r& A4 V21: S6 G* V3 b7 n; m$ x3 L3 D$ H
    三、改进的算法——KMP算法
    & T9 f* c9 \: D. l朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。$ n& D; D$ s  @- A8 L
    一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。2 L8 s: M/ c- K
    朴素算法中,P的第j位失配,默认的把P串后移一位。6 B" T) _0 K; j- t8 _0 G
    但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:
    2 c/ C* z7 a/ w; Q) v& k1 g- |3 n' W$ G& V+ g% p
    222.gif
    5 x, h& J8 K+ T& x" k' E" v7 _& X# |! K/ U1 T. }: r/ M

    . H8 m5 F. v$ y' g7 B$ u这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。  r2 H9 d! `6 a, o, f; R% {6 D
    7 U5 h/ M. e, q& G

    ( @8 j: Q1 ^! K4 T相比朴素算法:
    1 H  T4 u: Z9 }- u# _7 Z, N朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)+ \2 t. l0 ~$ Z5 L
    KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
    $ I$ y" U+ F- D) c* O, h& f# I# \# P/ V0 n
    % ~2 a# q6 D, F+ B1 Q
    而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)
    * [1 L" y% M* j; c9 @
    , _% {/ c, C- F- f# S6 w
    & P/ N9 W. o* N
    开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)
    : ~* z: c9 @3 c! H$ e8 j; s
    3 W! [) {; l0 Q# N' @! o

    8 ?3 z: Y6 f, C比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的# F8 G: t4 x& ?  b# J
    333.png 4 D5 C% r3 r+ n

    1 [% j; V" q8 E  C
    5 M: [3 O+ c' |9 z; P) W* H6 ~
    假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”
    3 l5 j9 p  P  y% I: Z3 H
    * K4 C1 T' t1 f5 U! G
    4444.png
    1 ]6 o7 Z6 s2 ], I) f* Y1 e5 M" }2 a% L. a

    + u  t7 \! [' j* f0 s6 U6 N5 E# h那么可以推出,P1…Pk-1与Si…Si+j-29 z& d. y8 a( V% ~( V
    3 r0 Q- x/ O( u  @4 U
    555.png
    & J  ?' n$ i4 _( D# ^. `8 T- L/ k+ l7 {
    5 i  p; k. M# c9 s
    显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:& h3 i) |% J' l& @
    ! F, N% I+ |3 Q0 B  g3 H# z* w
    666.png ) |, o8 `9 j$ x9 m8 B& p# h' o" _4 g

    $ s8 U7 `# ~& M# C

    : U) Z* r0 U( M1 W& w+ U8 a为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。
    ' N5 {+ i  {2 c2 M3 Z
    # U0 M4 {5 _8 D3 R1 N- L4 c, \
    : ^( H$ a" n4 N( D, ~0 h
    最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。% a9 ~* l; o5 U6 E5 o. {% w
    + x5 ~7 o4 E- q2 m

    + F, \5 l- Y, G: E  W四、手动写出一个串的next值" v$ t* d2 s) r' I' a: u  p; F' t# j
    我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:, u* [" \2 g1 @: l2 u

    7 E4 h% f# j8 q5 y
    7777.gif 3 O$ m! I! `3 x' P: |
    这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。/ K/ R9 U  L1 Y+ F& _6 [* Y
    ! ?6 ?0 j+ U! Q5 o, T
    # s( A/ t$ N3 A' k9 A; @
    通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:9 i  e0 N4 k& Z& A1 m

    3 l! T( I9 _) I9 T1 ~

    + I6 \, Q7 }- B% G7 {2 K
    8888.png $ `& Q* T0 u' M

    4 y: u5 `0 ~- }: v) T. @五、求next的算法
    0 ^# o; F" s2 a8 {终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    * V, e( H  q% \# g- K6 I% ~) j+ H# i
    & S( r+ L2 e8 I! V( t
    int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度
      i+ Y  |. }% R& ?; F    next[1] = 0;  s$ H/ c' c8 `) ~% }* _) c9 B
        int i = 1,j = 0;
    5 n) e( t7 ^2 E1 s6 r% t    while(i<=cLen){
    - B+ d# I, T/ T( v        if(j==0||ch==ch[j]) next[++i] = ++j;
    + X: l2 `/ R$ q% d8 h# x7 @        else j = next[j];
    5 W  q1 z2 ?- G7 d    }
    5 Z  |) k8 P/ Z, t; ~}4 t8 i6 R5 Z9 ?6 w- v- V+ H$ p% \
    " t" a) e: U8 S9 E- e. f6 y6 P
    还是先由一般再推优化:
    9 S- {/ K, k! w; }5 [8 F- |直接求next[j+1](至于为什么是j+1,是为了和下面的对应)" z% E' K5 b9 O2 t& S
    根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。
    , d! p6 E: G' N0 s5 ?! V# S不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:
    3 I: S* m3 d' I' J8 `) Mif(P1…Pj-1 == P2…Pj) => next[j+1]=j( ~  K5 G! T. R+ W; m" m2 j
    else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1
    0 r2 V7 D2 `, K5 ?7 ?1 Uelse if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2
    2 A* B3 H5 w) l0 z1 g/ J* l( x* M1 I5 Q. B$ \( B
    2 l+ D4 A3 S; t* `3 z+ s

    4 t) l# x# @9 e% ~7 `else if(P1P2 == Pj-1Pj) => next[j+1]=3- E5 @4 H2 {- p
    else if(P1 == Pj-1) => next[j+1]=2
    " `& [' w- l8 V- t/ ielse if(P1 != Pj-1) => next[j+1]=16 F/ I! ?+ S* t2 [1 y. g- U( G
    每次前去尾1个,后掐头1个,直至得到next[j+1]
    ! [8 p8 J2 [# U' J! e5 q! j
    * ~) I- R# ?/ S- p- U9 G
    4 A$ h" b% Z1 C9 G7 u6 u5 \& J# p3 `) {
    再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    : J8 _# U1 n  j: ^. ?但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,1 @# {) l8 g, j# g- F- L( G1 N- _
    ①next[j+1]的可能的最大值是多少(即从哪开始验证)
    - C# P6 t& H% l7 g8 u0 L: T/ L②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)
    ! k: }  E! u+ \# C看一下的分析:
    ! X  F  z4 l  r8 X2 |: c
    : B, H1 r6 K3 s2 q% G( o$ e# ^( m8 Q
    5 B3 K( R( ~! X1 E; M( X
    1、next[j+1]的最大值为next[j]+1。7 E' a6 {; S' g$ F. o( S
    因为:
    0 O- U4 f8 w5 I# F# c* H: V5 f3 a假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。" Y, j/ T$ b9 B' {5 X# |" L& z0 M
    如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1/ E- h4 k# m2 E$ j$ S1 a
    2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]
    + S+ P9 G4 g, k! p& j' P* P, k/ S这里不好解释,直接看下面的流程分析及图解
    , Q! \, U% O. s+ @
    5 A8 Y  e# h2 q$ w3 ^0 a  I' Q
    ! I% J9 g' t4 I) N& D( w
    开——始——划——重——点!% S. M% _* g6 E& j/ w. Z
    从头走一遍流程
    . ]2 ~* g/ c7 \  a% j% r①求next[j+1],设值为m
    - F6 }% d: d0 ^1 P5 L7 w②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1! s) |( J* h) B4 ?) O/ O# P
    ③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则; l, z& ~1 j3 r1 ~9 o
    ④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1( a. Z/ X2 a  U; j% D' \. o
    ⑤第二第三步联合得到:- t! J9 }& D' V  s, P3 H+ a6 r8 `
    P1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合
    4 O  P6 ^* {$ u" Q1 Y' A⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
    . X+ b: k5 c) n9 v) j2 m* [' u: b
    8 X2 J; @& z" }& D- p
    7 i' O: ^. G0 N5 ~0 K! o2 ?' G
    上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
    . ]; v, e. z5 f1、要求next[k+1] 其中k+1=176 ]6 l3 l& b' |+ r5 u) @
    999.png
    2 O6 U5 P9 ~7 D0 ]! o. ]
    3 O: Z4 b0 J! X7 L* o# i# R
    2、已知next[16]=8,则元素有以下关系:
    - S: {! I, F) B( X  E5 [6 t* V# k' \
    10.png , d& T, h. l1 `; ]* N: ?9 A. n
    / \2 J" Z6 {/ s
    3、如果P8=P16,则明显next[17]=8+1=9
    # T- d9 A( N! G/ M; S  Y+ h, N' p+ R4、如果不相等,又若next[8]=4,则有以下关系
    8 p0 T6 c7 M' a) d6 [6 W; q, S- l% B8 ]/ K* |0 X" ]1 r: \, G
    11.png
    2 E. N) A  [8 K' Z+ ]又加上2的条件知* a+ W3 _( z/ ^
    " R  V9 b& h! U! I3 c: t; u7 ]
    12.png
    - h, _. ^% f' w主要是为了证明:+ m- }% D* ~! J4 _1 e8 G+ ?- e+ `
    13.png
    / a  d7 Y# b$ `- B: l
    9 O2 _. g+ w) w; d" r, S
    5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推7 w* s! l4 Y9 ~6 B
    6、若next[4]=2,则有以下关系
    6 J# [' h( A3 W; F" O) o/ H. X
    14.png
    & M$ l8 F' T2 k# y  |$ t

    2 j: N& K$ T3 ?  ~! v8 E7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!; G) F9 E! W/ o) Q  n) F& D
    ————————————————$ z6 _- {7 D# A0 @6 F. s
    版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    . B3 O) y% X" Z/ e4 K6 e原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411
    " {& |8 C( P/ s% g/ [+ W. K, k* k
    2 R8 ?0 Z# ^- V
    0 J& u3 e) c% d( U" n: o/ ]5 a9 t
      F, }/ z$ [9 m- p* k0 ?# |
    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 22:01 , Processed in 0.598459 second(s), 54 queries .

    回顶部