QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 2615|回复: 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值算法
    * b" E" B2 A' T. Y, H- u: I! B: t- W8 v9 J7 j% n
    大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。6 a+ j8 R0 i1 N* K$ G
    ( D5 u. U3 t. ]( L7 S
    - G8 H" H/ L) x3 E9 v% d
    本文的逻辑顺序为% Q3 p; @. t- {- z" n2 B0 \5 m
    1、最基本的朴素算法) ?( Y' T1 k) h  u2 C
    2、优化的KMP算法
    * y4 Z; V! A9 @3、应算法需要定义的next值
    ; `7 }' r, B0 j. g3 ?9 V, M4、手动写出较短串的next值的方法1 T' J* `! }7 f) m* r
    5、最难理解的、足足有5行的代码的求next值的算法
    $ F% t( n+ w* x( v, Q所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…# a, ^" J# L, k( C9 v: O5 o4 G' k

    * {7 Z* d6 A+ J; d4 g' K
    : D4 D  L! _0 ]* U  Y
    一、问题描述
    % y4 J' Y9 k! f; D# N给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。
    ! b9 Z- ^- j; |4 _# Q/ h! L; j+ _  F% v

    ! e; X! N6 d; w# ^6 y二、朴素算法
    / m! p/ H2 L3 f5 k# _: ^最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:+ X9 T* G- C  X( x% k7 |
    1111.gif 4 i9 O4 _& T* Q( U( x  c6 Q! T6 g. _6 @

    % a# Q) P8 n& S( M
    ; M) g, F$ L8 i  d0 A0 J

    4 C) U1 i5 H& W  W) C7 _, e这个算法简单,不多说,附上代码
    + U9 E  |2 y  w* v+ r3 W: _1 S
    ; P) L7 f6 q: `
    $ X# \$ b, X2 L" o$ T- i
    #include<stdio.h>+ n  Q* W! J) d9 n! J
    int Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数. M; R% o2 Z  D, a1 o
        if(sLen<pLen)return 0;
    * N/ p* ~9 Q8 ?    int i = 1,j = 1;  Y! ^; B- S0 f& |3 f" _
        while(i<=sLen && j<=pLen){- V$ D7 x0 p8 S: {% b
            if(s==p[j]){i++;j++;}$ r, q5 [7 e  Q, j, S3 o
            else{
    ( Z8 f( C' O* Q& ?# z& k1 L; _            i = i-j+2;' n. M* c6 I3 Z" D
                j = 1;! a: C& [' t# W7 ]& ~
            }% z# x! `$ j0 z2 S) j! L/ F
        }% ?0 q0 k) u7 o3 L6 m
        if(j>pLen) return i-pLen;
    5 H$ b( n0 C9 u3 S" @$ d    return 0;
    # l; x/ o; H( a}
    : ~7 b7 [. o# y* ]: O& z% k7 s% Bvoid main(){3 T" O0 _6 |, p: q; {2 l2 E
        char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存6 u( h8 e6 I! D4 y
        char p[]={' ','a','b','a','a','b','c','a','c'};' t) |' h( P1 C  W4 C
        int sLen = sizeof(s)/sizeof(char)-1;. l- P& f& M, L: S
        int pLen = sizeof(p)/sizeof(char)-1;+ R! T' ?% S/ p/ N* ~! Y  ~
        printf("%d",Index_1(s,sLen,p,pLen));
    3 y9 D' O3 S. O4 V$ F}7 j( |! U$ ]% E. }- @
    1+ P, [) {( a  a1 ?
    2
    ; q' t$ ?" T6 I/ |9 V6 Z3 }' f3" [  ~4 I5 S/ n4 A4 f% o
    4
    ( h; w1 }) f2 I( d$ X5) `! h' T. l8 |3 w7 \+ J3 z$ t
    6
    % q8 E! I, c0 f7 L/ L0 S0 x7
    9 y. }: H: o1 p  F83 H$ [! h  S- R+ Q/ _7 L
    9
    - a: Y- f, F9 R& o. V1 H10: Q6 v  \7 J2 H  B
    116 b! m' J. i$ t! P' D! z
    12
    ; C& H; v4 s/ g6 r( w1 b9 g/ H( q13) A+ |. e* l, W/ h3 w/ G
    14- b5 q, o% N2 O) L0 a
    150 g8 V. k- t0 ^1 J7 [. S
    16
    " _8 G# _3 v5 i4 v; p/ y1 P2 M17
    8 Y, C0 }' m% h18- ^/ U0 r9 \! [8 ?
    19
    % c% k6 W# k3 [; ~- C# \- e* @20
    5 b8 T3 ^5 F! f6 C" Q214 D2 s, j( E& _
    三、改进的算法——KMP算法- P7 j2 y  v- q5 Y5 j9 o8 k
    朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。' P* Z6 J+ `$ ?' C
    一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
    # H  I, q+ k1 ~. e朴素算法中,P的第j位失配,默认的把P串后移一位。& U0 C5 ?2 _  Q1 v1 n
    但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:+ w9 f% B3 Y: f2 O
    0 }) K) R" e) g8 T; E
    222.gif $ H* U/ w. m! ]) Z# D4 s' P
    + K; g/ ^# |9 I- G  U$ n

    , F$ u" K6 G$ F# T( Z$ G, X这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。
    0 f, o2 ~3 N& Y+ d4 _$ Q0 O; q! F: N6 U
    2 W" l$ d) L6 p% s, }, a
    相比朴素算法:) X, E9 |, ~+ O/ D8 \4 X
    朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)( h8 p  {. P, {
    KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
    5 g4 ?9 @8 W. d( h4 {% d: M, p
    ; N. n8 X# t9 U
    * q5 @6 {- b$ Q# t
    而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)' d% z! v1 S  v
    ) a. P' {6 H$ g  J( C" @% }! Q
    4 m: q4 _0 t# m3 c
    开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)
    * i% R- _/ d! @! l' k2 d' }8 N! N1 V+ \% U4 {3 X: d/ r* x/ ]

    ' N' h& |* a' R9 r比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的
    % j# Y, C- M, C; k
    333.png
    6 D0 N: Q2 {7 m# @3 }) F, d+ i# V4 k  N+ E# Q& l

    ; M# P) r4 h% V: g. U假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”
    ) v9 }( u  \7 C8 i3 P  C" [& C4 [9 X; r2 L
    4444.png
    1 i3 s+ ^1 ]0 o; N) Z
    1 q+ s! n4 J* g, c

    2 E: J1 {4 ~! k) ]3 T! K4 w, s那么可以推出,P1…Pk-1与Si…Si+j-2! h4 Y$ x/ N" H3 v3 [3 \3 x, g
    - b) a4 X. c& c* g, I; m( ?
    555.png ' d! `* s, u( ]# w: d  e( P

      s; H9 q4 g! E
    ( G2 S8 m8 u4 S3 K2 C/ m
    显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:
    ) [- Y# N/ f# R9 S
    % o5 x- N8 P  M4 F) q
    666.png
    ) I; [, Q* ]: n. p3 q  G, G) ~( n4 G( a

    , H: _, R2 Z9 V3 b, r为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。
    * B( v$ Q/ k) P: m8 d; N
    ! b; G1 U. L; I0 ], m& Q7 X! y

    % H) z! f, G- t" u最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
    9 X$ D( [: P: I( P& l( y$ m3 n7 n+ m& ]; p
    & ^0 P4 n3 {3 F& y
    四、手动写出一个串的next值
    / j$ s; N1 v) V! }) Z我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:$ d( T# e: D8 O3 }" ~! k, m
    6 c4 @3 O+ s# v$ ?
    7777.gif ! R& a# q% E  a; A7 Q7 F/ q& m7 E  f
    这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。7 D$ m2 z/ a6 m* }

    0 b% a" \3 ?9 Q% x

    % p' }" r3 ~; t% H" L通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:
    & I/ {! N; q; t# f) v; u7 z+ L# t0 N% Z7 B+ Q

    - a) R' \9 i; c6 {# c, P: G
    8888.png * M% g4 P: x6 W5 }, ^

    * S/ _5 d; V- R; r- C; W; x! c- r五、求next的算法
    ' C% K3 g2 |# E) b1 F终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
    9 O% L1 o# K( q# o$ F5 F5 {# i3 x; u$ g# u' D4 L) j
    / A% `7 i% ]5 n
    int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度; z( h  y! A. l* o! F1 A4 S) f
        next[1] = 0;& H$ v. c2 z' G. A
        int i = 1,j = 0;+ O- e3 P; u: U7 T; D
        while(i<=cLen){
    - Z6 B, z0 O9 m2 l        if(j==0||ch==ch[j]) next[++i] = ++j;& L) V, W' [- q4 T6 `" ?. T
            else j = next[j];* Z& m0 A5 K# Z7 O9 \  l
        }
    # A( T% A* V% N4 c  y1 C# c0 [( p}  g  b4 p+ m6 E0 j4 y
    ' y8 d! \0 T! u# J8 M$ w, l
    还是先由一般再推优化:' E/ \+ x: _. ?/ Q
    直接求next[j+1](至于为什么是j+1,是为了和下面的对应)
      z1 o( E# T- N1 U3 Y! }) g根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。
    6 O' \2 J: S- m' c& v, ?  f不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:
    & m" m; n9 R) Bif(P1…Pj-1 == P2…Pj) => next[j+1]=j
    / O3 X0 {1 k& w4 Belse if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1
    / m& y6 W/ J, kelse if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2
    ( c6 o2 J" P. _7 W& ]3 n3 Z8 G
    * A/ m! H  {9 Z- T3 h& m! o4 d# M
    7 f- u0 n8 Z, }, |* \5 B0 b
    ; [. N( O& _% O" z# _; Qelse if(P1P2 == Pj-1Pj) => next[j+1]=3
    ; H& \$ ^& O7 Q* c' ?else if(P1 == Pj-1) => next[j+1]=2
    2 R0 f: |6 ]$ l) m, u( K8 eelse if(P1 != Pj-1) => next[j+1]=1
    0 J% f+ u/ e3 Z! [; t9 l每次前去尾1个,后掐头1个,直至得到next[j+1]4 d% x( d9 K* y$ k
    3 I: e% d3 @3 W9 c+ O, T
    - ~( o& T. _7 f) y5 u5 c7 |
    再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
    " {5 d4 S- A5 ^; {, d但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,% }  Q# m+ y' l' h- n
    ①next[j+1]的可能的最大值是多少(即从哪开始验证)
    6 l; N0 j( [* C$ N②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)
    - Z8 i6 b4 Z% v* R看一下的分析:' R0 e+ `$ \9 \" x
    ( |9 a) ?* w, `2 t# m8 H8 w( U

    ; B( z3 W  ~0 q5 b. s1、next[j+1]的最大值为next[j]+1。
    6 X, L7 O9 z7 j' b- C. G因为:4 [) s  T4 v3 p; w& C0 o
    假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。; A* f/ U' o4 m8 k
    如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1
    4 f1 [$ Y# F1 R- g: Y$ X- X/ m2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]- L+ u. z7 s6 z" j
    这里不好解释,直接看下面的流程分析及图解( j6 Q- V& q. n, A2 ]
    ( f5 q5 E! l! G% ]

    * Z: F; j( W& C, X9 ^/ v开——始——划——重——点!4 e9 r# b2 ^8 B7 ]8 M$ d( n4 g
    从头走一遍流程  L! o5 z* D. v% w
    ①求next[j+1],设值为m9 b9 V' v  q1 P4 Z5 G
    ②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-15 J& |! B' h; O6 ]/ j5 i3 Q" ~- @
    ③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则
    ' b3 G  A  I7 Q) _4 V' o④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1
    4 _5 ~  {. \7 _- Y⑤第二第三步联合得到:5 B7 k5 \" W' m
    P1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合
    0 n5 a2 [( h# ^( {⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
    0 d+ ~6 [# q  j+ i  s) w, U  E; h. \1 b3 N1 z8 W

    / n% [7 t- V( b上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:4 Z' [$ ~% Z- L+ I. t- m1 O3 {8 s
    1、要求next[k+1] 其中k+1=17
    # @- g9 D2 W' t3 m; e
    999.png
    , v' d% x$ v: H: g

    ) s1 \/ y+ G% I' W# \" _2、已知next[16]=8,则元素有以下关系:
    , x7 k+ }1 Q  W: O  N
    10.png
    2 T4 X; i" R8 _5 Y. x% O3 R% f
    : x8 h( y0 D* h2 D- H- i2 d2 P
    3、如果P8=P16,则明显next[17]=8+1=9
    5 f! z  u3 i) p4、如果不相等,又若next[8]=4,则有以下关系
    + m. z! K5 a! s8 f' j1 G
    1 C  c. s# m* p4 |
    11.png 9 u5 g1 p/ c4 T! [) x4 d$ a
    又加上2的条件知2 _+ z; |5 e5 z) ~8 e- s% X% W
    * o" _2 P' A9 Q7 \$ E8 f/ Y
    12.png
    ; Q: C0 K& e( ]& E主要是为了证明:
    2 i! S6 _: _! z
    13.png   S1 d$ X- p3 `, D. j* w" V9 R

    ' i7 Y* V! _" L# Q) k5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推
    ) V$ ^1 T3 p9 t# b8 _6 r: e" q6、若next[4]=2,则有以下关系
    . z; S5 ~9 L( H* I1 x0 ^# |
    14.png
    7 i1 r2 f1 r0 z

    ) }* Y3 [/ U' `' K6 B3 w- J7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!
    + u. k% \2 N/ f) R————————————————
    4 k( J. F5 w: O) ]/ l版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。% D' m$ s2 t- X' i: h" S8 y
    原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411+ L4 V# r7 U0 t0 z4 \- y: w: o
    ' X" T6 k; o  E5 i
    8 w2 E/ j7 F  P0 {( m, j

    ( F; p/ S" Y6 X+ O3 A! F6 [
    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, 2025-8-11 03:29 , Processed in 0.657103 second(s), 53 queries .

    回顶部