数学建模社区-数学中国

标题: (算法)通俗易懂的字符串匹配KMP算法及求next值算法 [打印本页]

作者: 杨利霞    时间: 2021-8-10 16:12
标题: (算法)通俗易懂的字符串匹配KMP算法及求next值算法
(算法)通俗易懂的字符串匹配KMP算法及求next值算法
, f9 N5 w# r9 K" O9 ?/ c2 K2 i7 @4 R+ q1 @8 N( W5 \' x+ o. j" R
大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。
3 b0 r4 w# b5 `1 w1 h$ m9 X. i% v- @' R* i% P7 U- K

) Q' f" d9 V* R# k: F2 N本文的逻辑顺序为  U, }3 a: X0 s' u0 g! }
1、最基本的朴素算法
4 \  G$ E! ~% C2、优化的KMP算法
2 d" t! m- H3 k* b. \3、应算法需要定义的next值' @9 ]. Q( e9 S
4、手动写出较短串的next值的方法
* z! @# c4 x- L9 \! y, K- P" |5、最难理解的、足足有5行的代码的求next值的算法- O, Q* j7 m+ [8 @- B
所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…
: L4 H- I/ W$ H; G/ F, ~3 z6 C7 X- p8 V+ p# o8 M; p6 w

+ s, p7 `6 P' x. W! ~3 I一、问题描述
! M# g5 i6 U( i9 J0 u* _给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。# U* n1 @1 u, l! q- H6 E# I) z. v: |! ~" J
, c/ Y$ }3 p6 z# x  R( S

# P& t5 N* B7 x二、朴素算法$ V( ]' u  Y& V" K- J; |* l! D
最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:
" x0 x% _# w+ @+ C- }2 L# V2 S5 n 1111.gif 8 p6 [7 B4 S/ ~7 Q8 }6 e3 g

. H5 a( ?( U1 N2 c1 ^0 D2 ]' w" k) o% L; T' P# e% G$ L  s9 J
& a2 F% y5 K! ^
这个算法简单,不多说,附上代码6 t) P* j* t$ O& K# u

* h5 F6 Q. k! a4 _. C# S
) Y1 X0 H) f/ q' Z# O" w5 L. _  @
#include<stdio.h>
+ ^2 y3 y- {/ ^int Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数
9 ~- J! d% l3 `5 o; I    if(sLen<pLen)return 0;' U& c% j8 ]* {3 U! n# |8 q
    int i = 1,j = 1;" K' N! z/ Q7 j% d
    while(i<=sLen && j<=pLen){- q2 ~" n* R4 S' N4 W+ ~' c
        if(s==p[j]){i++;j++;}
  Q" R0 c1 m6 ?! S' y0 j        else{
* H& I+ B5 r. x+ p            i = i-j+2;) y6 T' a. x0 C0 G1 g' V( c0 ^
            j = 1;
( ?9 a4 u/ f' a: I  f0 s        }
5 k/ G9 e" W, D1 S& p    }
) d6 u' n3 y& R$ {    if(j>pLen) return i-pLen;2 q8 k# i6 k' h6 A
    return 0;
! o+ M1 [# i; B. p( f- P}
4 ?9 \+ t' F% f; Q2 A# Z/ nvoid main(){
& F5 `$ M& u& V7 x0 X& `" j6 Y    char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存
' X7 o& A/ K: _7 u2 C- m    char p[]={' ','a','b','a','a','b','c','a','c'};
! L2 _  L! k- G* F! U# k# o7 d- a    int sLen = sizeof(s)/sizeof(char)-1;
+ M: w8 G3 o8 w  p* U/ ~    int pLen = sizeof(p)/sizeof(char)-1;
9 d: @5 O  ~( n6 Y) \' e8 T    printf("%d",Index_1(s,sLen,p,pLen));
! C( z$ V6 Q; ]}
6 K- ]" ]) d; @1
9 \$ {* w) M. W; X21 `% w" _' ?+ J
39 f. `/ l% N' p
4' g5 R1 s4 @* c
57 U! _8 @/ _' h6 z. b1 ?1 x
6
* A' j- _5 p3 `3 _) M4 f5 d! @72 O5 v" q0 y+ g4 F$ Y7 S6 P
8$ I+ _9 D# T& u: n) M7 H
92 {0 @* R" H- C& |: E' T
10
& R1 S  y# A+ h  p7 E: ]11
; e& T# h* l1 S5 c  `9 y7 `& G12
* C* `" p+ {* A13
+ w. H: j) S, H" M14# w- c2 T/ ]& c+ H) ]3 o
158 K' n+ \$ z1 h- i, _
16
& A- {/ z& U2 d17
* x2 r. N) R' i% z$ P18
5 L6 {" g; T$ `8 [% @+ _19$ d- }! `* @& @+ @7 |. X
20
, ~4 \5 O8 L1 _1 X21
! G, p* X: e8 M, v$ D2 i三、改进的算法——KMP算法. `( b6 S  I$ s& f& x6 g) Y" b# M
朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。
( d* r, u. u" }' B一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
' S% H* `: t) ?. I朴素算法中,P的第j位失配,默认的把P串后移一位。/ B* _. h5 i: h  N
但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:
& C0 n/ \* D: C
$ r! H4 Q* u, ^  S$ y4 m0 |9 m
222.gif
' E* p8 R# K2 h1 n/ R4 h
7 Y/ `  I# u5 F/ G% N
* A+ e% O0 B# I7 P9 G7 z* i. R
这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。, c/ v. _& A/ A6 Y8 z
3 i1 c' y- m) B

( g+ V+ b/ c; W5 G0 Y相比朴素算法:. b- I! j4 P7 F" U% S( V& T, c- D
朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)0 S# I" F) s& r' f# k
KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高+ |1 @0 z0 q. [3 f( J7 I

  S; k+ s/ r6 }) Q4 g
+ l$ Q4 j6 _4 e! g" n: S
而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)+ M- r, R% ~4 V. B4 a& Z
6 b1 X4 _7 Y9 l: ]' `# Z, E$ m  x
/ C) v  a% O+ {1 S
开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)
5 T+ P" r- X) A! t- M& j7 p" W4 B; d0 V; ]

0 ^7 j! R+ K- D) j1 Q比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的
" D4 p% A; [6 m
333.png
& w0 f/ e/ m4 ^& j& L9 \  K( X
7 c4 w* \9 p% Z7 a7 @+ `5 y
. o2 Q1 R& J' J5 a/ s
假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”
% c1 _. H: o' ^2 W6 X8 s5 w1 D6 {+ c+ s
9 M) H* R4 p$ y3 X) u' M
4444.png ! w3 A9 C8 Q0 q6 ?6 Z
$ j) b2 {% U1 Y" {
; Y& K; j6 T1 G0 S: v9 b7 j, z
那么可以推出,P1…Pk-1与Si…Si+j-2
! }) {. f# a0 G# D. f
: ~6 }" @3 ]4 O; P4 r
555.png
4 A& N+ O5 i3 z; ?; n
* K/ ?" [$ M9 h6 \0 A: b( o6 u
/ D/ G$ d2 p& ~8 _
显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:
8 j0 y. x! W8 s0 D
& V7 r1 S' N: y
666.png
$ d% V4 S5 p' q. q5 L, v; m% N  u8 Q
6 L0 l8 f$ O7 H1 X! s/ E8 p) \( N
为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。' e( z) ^* e0 E4 C% B3 ?- F
/ D3 w) v5 U8 w* [& Q5 r

/ `1 y/ E& M2 O, J+ O( n最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。5 ]' J1 C7 E* H) [& W7 [3 v
! |4 Z4 m! [$ |4 i$ q, t+ q" k4 ?

! \/ H- w: @: J. _9 |2 d四、手动写出一个串的next值* @# `3 C7 [. |, c1 f! `
我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:
. O' N* W. Y" I9 ?1 l
' u3 M0 ^0 }9 B# c8 p2 z1 w+ ^
7777.gif
/ ^, c* i: D% U/ J2 J+ c这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。9 Y0 U1 f' u' ~  x* D& O; s! R

6 \$ e7 q. F% M1 L) w% L" j
( ?; e- O: W6 {2 y  S
通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:
+ U( Z! z! I, j4 p
8 g  s7 _0 x/ S2 F

) ^$ I  d( P5 p( [$ I2 o. ?+ b3 h
8888.png : }* n4 l# X. c' T
- G# @( s! O- h( N3 Q7 \
五、求next的算法! L  g2 Y5 O9 L1 i( \. k
终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。+ F0 H& C' L6 c" s7 H3 [1 N
1 C+ u$ a' Q9 [

$ A+ B& M* Q( Y1 \  Mint GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度
2 k: O5 F( g! \# M2 y    next[1] = 0;( i- z0 @# @2 V* o3 u; Z" r; Q
    int i = 1,j = 0;
0 h+ c: J6 L0 M4 O8 Y    while(i<=cLen){. [' ?5 {2 Z. ~' ^$ P& m8 g1 G, b% L
        if(j==0||ch==ch[j]) next[++i] = ++j;
; z0 d  i4 b6 Y        else j = next[j];4 q/ ~- k0 h- p+ A! ^$ X; f
    }; l+ C: ?1 y7 _0 S
}
4 k# K; ]1 Z; M/ d
' H0 Z  H# s$ f% s) A# X! {$ K还是先由一般再推优化:
8 x5 Z9 ^6 R4 m" k+ m+ U直接求next[j+1](至于为什么是j+1,是为了和下面的对应)
1 m# @! v) a- t  m2 y) }根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。* ~7 q' F( c% W5 ^3 f( B' f! d& ~
不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:
5 n+ k: i( G+ ^0 [if(P1…Pj-1 == P2…Pj) => next[j+1]=j
8 I3 D7 e5 v, b# j. ?4 Zelse if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1
$ V% P! R+ D) e7 s! b8 z- felse if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2
1 M. V5 ^2 S- R2 l$ X
4 }( I) p1 W6 A" I, _2 Z. R' F; g3 H' J6 o" f* ]! ^2 }

! m5 h  X8 ?2 \  x0 h1 i2 T2 nelse if(P1P2 == Pj-1Pj) => next[j+1]=3& }$ W  {1 T6 M% S
else if(P1 == Pj-1) => next[j+1]=2* |" m6 H( x' S, w$ M  f& A6 G
else if(P1 != Pj-1) => next[j+1]=17 A- L5 M3 ^3 q$ \. ]
每次前去尾1个,后掐头1个,直至得到next[j+1]
  O5 t2 L( E1 A8 h- i6 l) N8 p+ V9 G% R
. D5 _5 D1 i( j
再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
  a. ^' K0 f4 ?3 e4 z但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,
( b) ^. a, S0 T2 P①next[j+1]的可能的最大值是多少(即从哪开始验证)0 {0 z; R8 f% n- R. V
②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)4 R# i  F, A; n$ {1 `) v" m
看一下的分析:2 X# q4 x# l3 M) _7 ^) x& a

- \4 P- {6 x3 l. q! j; H4 u
! A1 l: d7 S- k: X8 J/ U$ i( b
1、next[j+1]的最大值为next[j]+1。: U* ]  w9 J( q* t
因为:& P2 N5 i. T: f1 v3 u
假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。4 D% ~/ ~! [( x( ^4 j
如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+11 p3 h3 t- J: Z! Y* B' [: r8 X& P7 @2 i
2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]" V, u8 @' `  \5 J$ t& B
这里不好解释,直接看下面的流程分析及图解+ D0 j! X$ t! J

: m6 C/ ?! H- ?; O1 r+ j  X) {

" r6 j  o2 q- I5 u开——始——划——重——点!* O9 L( x9 r- O
从头走一遍流程  l. }) b" M: C
①求next[j+1],设值为m1 o5 k7 F& k9 l. r) Q
②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1
  @) R9 F( v/ Q4 K( j8 |( ^③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则
+ k5 R0 F$ y; B0 W! E( o- [2 ?④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-11 v1 ]; X5 a3 \5 ]) Z
⑤第二第三步联合得到:
) I! L- C# g# e6 u: zP1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合
* ]- ^( N$ d5 k* f* h- \⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
+ T& @: H" g' ~, M8 j+ X7 j9 S4 A1 W# b+ g; s7 y5 G" N7 F' `

3 ?. s; S+ a3 u0 m+ h# A0 P) ?上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:0 y- _# U" \0 @) C
1、要求next[k+1] 其中k+1=17. d5 J- f4 N/ ~( I8 E1 N
999.png 0 o* v  P5 O$ B: |$ n8 j' f: z! p$ v
/ C" F& h' u8 c
2、已知next[16]=8,则元素有以下关系:( F2 \+ t, l; `
10.png + L( }: [& H  N6 C
2 G; M/ D% V4 r8 r3 R
3、如果P8=P16,则明显next[17]=8+1=9: M4 w$ F9 k: `1 W7 W' `
4、如果不相等,又若next[8]=4,则有以下关系6 ?6 x  [( Z$ J9 b0 W
# Y; D1 _$ W* ~3 g7 `& M3 s
11.png 1 u5 [& x/ [; N( ?6 M1 T* j
又加上2的条件知
  r5 N4 ?. Y) B) j7 X) U
% V! V1 N- K2 f3 P. |, d4 a6 ]- L2 u
12.png 6 m% `2 g1 i" ]8 R, G: D% o6 k8 C
主要是为了证明:! v* \- _- r# q: ]4 d& z
13.png
5 u" Y# K! B4 t/ Z) Q* B4 P

! {4 A3 N. q' z# D' T5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推
+ m9 T4 _! m$ C( p% J- {$ N# s' d% n6、若next[4]=2,则有以下关系3 c" Z; U" n' q6 `
14.png
0 Q  T0 p! g2 j
5 z7 E- _  ], B5 O# x
7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!3 X' |/ J3 P$ G9 @
————————————————4 y  K8 }  |) x4 Z: l8 \. W7 y
版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
, ]  b4 H7 Q$ F( H, O原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411
* n" w2 I% O; N: ~; u$ X7 [8 V0 t" x% F. |2 {3 X

; Y: }/ Z! J! T/ c/ c
& T* v9 Y; P; R





欢迎光临 数学建模社区-数学中国 (http://www.madio.net/) Powered by Discuz! X2.5