- 在线时间
- 1630 小时
- 最后登录
- 2024-1-29
- 注册时间
- 2017-5-16
- 听众数
- 82
- 收听数
- 1
- 能力
- 120 分
- 体力
- 555830 点
- 威望
- 12 点
- 阅读权限
- 255
- 积分
- 172122
- 相册
- 1
- 日志
- 0
- 记录
- 0
- 帖子
- 5313
- 主题
- 5273
- 精华
- 18
- 分享
- 0
- 好友
- 163
TA的每日心情 | 开心 2021-8-11 17:59 |
---|
签到天数: 17 天 [LV.4]偶尔看看III 网络挑战赛参赛者 网络挑战赛参赛者 - 自我介绍
- 本人女,毕业于内蒙古科技大学,担任文职专业,毕业专业英语。
 群组: 2018美赛大象算法课程 群组: 2018美赛护航培训课程 群组: 2019年 数学中国站长建 群组: 2019年数据分析师课程 群组: 2018年大象老师国赛优 |
(算法)通俗易懂的字符串匹配KMP算法及求next值算法/ O1 ?2 C( s0 |( t
, }' R5 a( `, j7 r大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。& _3 x5 \8 A4 d
1 V& U- Q- F- G" g7 n, l
4 ?( |- f- d0 D. P' E/ U) M本文的逻辑顺序为
. L" U- y1 A* I L7 n1、最基本的朴素算法
+ g& o8 G9 D4 W6 F2 t2、优化的KMP算法
. e/ F) F. E5 ~3 e3、应算法需要定义的next值
; R2 q m* I- p2 x6 @' U4、手动写出较短串的next值的方法
# p; B$ Q! x( |8 p8 @6 K5、最难理解的、足足有5行的代码的求next值的算法) t& b& |6 O) U6 s
所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…
/ r7 ?# Q8 B) `, E9 ~$ F; Y. p H
$ x% A7 v" n+ I" L' @' _* Q S; b
9 ^( Q/ `: A' t; a# f) u% z5 `一、问题描述
) A- H4 F9 j O5 |7 o5 G) b给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。7 N+ y3 ~6 I9 `$ [$ A/ @! y3 A
1 A7 T' G. j* @6 ~0 `; d
8 A$ R$ c2 O Z5 b! _( X. e
二、朴素算法# L3 c7 d% t0 m& u
最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:1 \2 l7 U6 d3 F- I/ X
8 i' b# ^) V/ }' u
" P! f' m b+ V! N
9 c( M0 }0 Z+ ?1 t2 N
! ~1 y1 \& D2 m这个算法简单,不多说,附上代码' w, o7 u" Z& h3 u& U
; ?% _3 n) b5 Y$ Y7 y. S5 Y9 K4 Y% \4 C) S/ d
#include<stdio.h>
$ x* \" i; r6 r# O! b2 U" Iint Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数+ L5 q. B- S7 _3 J; d; m
if(sLen<pLen)return 0;3 [+ P0 }7 ~& ]0 b% w+ T( |
int i = 1,j = 1;3 d+ k6 t' E( k2 a+ p! Y9 h
while(i<=sLen && j<=pLen){+ R$ Q' [, l! y0 C% S. a( L
if(s==p[j]){i++;j++;}& O6 N: s |' r6 O" i
else{! D9 w" w7 e7 W! a* p* C
i = i-j+2;& U7 ~) H4 u8 O/ t
j = 1;( h: H0 E l/ `, w T: @7 Y
}
; n# H. R+ H Z% _" P }
& a: a% Z5 s4 L; w% z if(j>pLen) return i-pLen;* b5 q5 c- G2 s! N9 _, W& k
return 0;3 ]) @# v8 i' P
}1 ^; s7 ]: j0 C0 Q& i$ T& U5 B. U) E0 ^
void main(){/ f" x6 W2 E$ z) J" U
char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存
- z( N4 ~' J8 q8 s. V# P: w. a' Y char p[]={' ','a','b','a','a','b','c','a','c'};
/ M. X: L# \. X' I8 y int sLen = sizeof(s)/sizeof(char)-1;2 d1 r' V) s, ]! H5 h9 I1 S
int pLen = sizeof(p)/sizeof(char)-1;2 p0 C, ]9 e' M
printf("%d",Index_1(s,sLen,p,pLen));5 W; J' N9 X! q! t# C& \# a# p
}3 u9 @( ^- v. @2 [) p
1; M$ M* d5 V; W* V
2
/ n$ ?4 `- x: F6 v( V' {3
5 V- K J6 D# X& K5 g- n4. C/ `; ^! G: r) Y8 o' I
5+ l8 k" u, J8 G
6, O& H' Y7 J3 a
7
: K# U2 z; q4 ^9 I8" t* p3 t" k7 \! C$ X9 X
9
) C* Z* W: P7 @10$ |( o' l6 e# p1 o- y8 k. J
11
; @9 M: |+ Y1 \6 z: Z' c; F5 x12
3 [7 a$ W' \9 W: B1 k6 x1 Q13
% {- N3 {- t, ?& N2 F9 Y* H- ^14
3 J8 Z; @ V9 E$ w; n7 j/ B15
, V8 V# l$ g- H* R- V! _# s16
$ f/ P7 k3 h- J. P17( k, n6 o6 {5 h$ V T% P" T3 P9 V$ J8 r
18
3 y1 k2 W- Q2 M' J/ G7 C199 N' ^1 L" C' y3 Y# Y8 @
20' Q# P5 N! Y- u. E/ `
21
. a1 A1 Y; l6 b7 b( L2 m5 R" t三、改进的算法——KMP算法
: i2 V& J, a- D朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。
$ u7 _9 R, x6 L, q+ U0 N" Z一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
; ^2 Q; ?+ ]0 R. A: g朴素算法中,P的第j位失配,默认的把P串后移一位。
% h, D0 G4 f: m2 R3 k但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:
' X, o _6 U7 G) q( N/ J
3 K8 T; c. R3 p5 h% ^4 f
6 o) S* [$ _3 E2 ~7 }/ o6 g- h# s$ i. G# P* ^3 \+ V
8 T. i9 x V- n7 v2 w. U5 J2 v! t, ?这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。5 _* Q n1 ^7 ^, Q( G% `: M+ Y; F
1 Q) W8 |$ c4 L2 y% ?2 |+ A8 k
8 W6 i* A# `, v2 t- w7 r相比朴素算法:
; \4 J" _3 s/ _$ f0 ^" a朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)
) q2 A$ F8 C' B0 z9 ]KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
5 p0 O3 G' Q, o- T2 d9 C$ W/ \4 ^1 P' l: }
$ Z; ~$ v0 N; v- m2 G( }6 ]& t而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。), g/ h& m1 i' U0 a: S
4 y7 k ^. ^4 e: l0 x: T W
J2 g) J) S$ S/ f) z. `' C% ]
开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)# j9 y# U+ c, |# Y x
* [: G* |# H5 i6 P
- u0 ^( t e; W
比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的
( o; |8 h4 R# A* o) w2 x' a& p3 {
1 r7 i6 R8 _% }) `8 E9 d
. C& T5 W5 e, F
* _( o1 G$ u6 M5 b
假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”
0 Q$ `8 g# q1 q" `, i5 H/ b% _7 N5 `3 G; x
4 ^4 T$ S% s) L0 C7 W
9 R- W1 |6 d( M! }9 |
: y. ?% `9 ]) F" g那么可以推出,P1…Pk-1与Si…Si+j-2( [" B B) K8 L- f+ `4 Y: Y5 d! K
! @* O2 C$ \" T$ {
6 c% d- Y" [) {( e; S7 S
4 y, ? f, |# T/ ]* x9 y
: Z/ H+ V0 i2 ^# m% G显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:) @6 s2 {9 ^0 g5 U4 o m! h4 ]& E
% c6 z y( j1 E
% B J! y; |& {# @/ S
' z& Z, c1 t8 ]) Z6 i/ Q! y
6 ]9 j) v0 X2 H3 b为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。6 i) y8 w# K0 J# Z w! n5 {
% r7 K4 g3 Y4 N( q) R% h3 u) `
8 S+ ^; k8 S+ x9 X
最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
% F' o% ?; H4 I4 j0 m& g( p5 t" t) a- i7 [# l
' f( y& c& } R# a- t' c/ J四、手动写出一个串的next值
# Y, a1 U- V4 s$ K我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:9 _# F6 A; e( D7 z
+ K' e% |) n7 g R5 x+ y
; `; b8 `5 ^; p' k' Z1 T2 Y; m' S这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。, p) P* S* K! E! M% X
% ^/ E$ h; d1 T4 k9 H" A
% h9 ]% G- {1 e" r" z
通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:
! i* u; j! @$ _. ?" B
; B9 |) z j. u! I c, ?0 H* R* i, s- P: @- ?- [
3 [. U6 @& y2 k9 w. X. a( w K; i
7 D8 a4 H/ L2 q8 F) n" N, c9 [
五、求next的算法
' _# S9 c' X( i0 Q' z: Y# T+ O终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
' j7 W/ S6 f/ _& [, w( f
1 W! X, C) r! C9 f k' l9 s8 }( t& l4 g9 H+ b
int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度
' ?/ U7 u0 B5 q* s1 ?2 Q7 ?" f' L next[1] = 0;
+ f* o, Q: a( C% o P) s int i = 1,j = 0;8 h. f5 r% h" r m0 a
while(i<=cLen){" `1 z2 Q$ }8 q) y( c, v- Y6 t
if(j==0||ch==ch[j]) next[++i] = ++j;4 S0 B# ^( ]( P2 i9 |7 j1 }
else j = next[j];$ m( P; e& x. G1 }) ^
}- @; }2 }) r8 N; J. T
}( P1 a. k1 I' r
3 O$ [; n: T. Z" h: g ^& ]还是先由一般再推优化:1 Z* s7 B* n g9 J: m; w9 k
直接求next[j+1](至于为什么是j+1,是为了和下面的对应)
$ w; X8 V! |# W [* Z; q& O1 v根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。: S X3 ~8 K/ ~$ }4 C% W) Z
不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:. S' W. C/ m/ ^; M/ Y: x
if(P1…Pj-1 == P2…Pj) => next[j+1]=j" C/ {" J m4 }
else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1
2 {% m7 u; @( |% F+ Z- Velse if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2
4 D8 D$ h& c) e9 [/ `1 `…
( a& ^4 B: z# Y4 \& Q: ]…# h" N& G" d0 N; B# J9 N+ `
…1 m! ~ d7 z4 g- l
else if(P1P2 == Pj-1Pj) => next[j+1]=39 x* H: m# r3 \+ d5 g* h3 e
else if(P1 == Pj-1) => next[j+1]=2! w d. m, h, ?
else if(P1 != Pj-1) => next[j+1]=1
0 q. ?3 z3 m7 W. x每次前去尾1个,后掐头1个,直至得到next[j+1]
0 W+ y% g$ }3 H. g9 ]
: ^# j5 p; \9 n- s/ e# `0 C$ u
, i4 A" K# h9 ~% `1 u: u再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
+ n' S" K) ^+ t7 D3 `, E+ f但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,' L. F% G; O: d+ F
①next[j+1]的可能的最大值是多少(即从哪开始验证), k, g7 T& P( r0 Z8 o @
②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)- N9 \/ t$ w( \+ Q
看一下的分析:- O5 ?2 m! v2 x# ?( z2 i9 C% x
! e) V/ P$ v9 M- K9 z( ~
- g( U; h! X* w* b& ~( M1、next[j+1]的最大值为next[j]+1。
2 d/ ~9 ?+ B, q" U因为:
1 S! K2 x7 [' `. t假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。
( o' j1 J7 Z# B5 {如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1% r' m2 t3 j& o% y2 o; x5 n) I
2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]) ?8 @9 a0 `4 N, R3 r. P) t
这里不好解释,直接看下面的流程分析及图解
* y+ u$ K2 v3 B8 e0 r
4 s6 F4 h9 \! \ u- w
+ k* i5 R- k* n开——始——划——重——点!
' I, m7 }# L& J& V: A5 N从头走一遍流程
; `" M1 I$ A( E: W4 f3 S5 C①求next[j+1],设值为m
+ c6 }. {7 Z. k4 @②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1
7 Z. N, Q I. K1 ?/ d0 q③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则
_& @1 s: H: u4 N% `* i" O( x" V④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1( X7 l6 Z$ a9 J& U* V& T
⑤第二第三步联合得到:- m, V$ L, ?6 J$ E5 d' m
P1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合3 X9 h9 c l! j: q
⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
. @% J) {2 f: n) Y: X Q+ G3 b" G H
, H) l- G2 m0 ^5 h7 u9 f3 V* ]
上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
4 [( l$ O/ t' g5 ~! r1、要求next[k+1] 其中k+1=177 L9 N- C9 A# A$ A1 F/ t7 \( i
5 Q' R* Q: t% v0 v6 j$ H
1 a5 X& n8 h8 n' M' j2 W2、已知next[16]=8,则元素有以下关系:$ N& }8 q0 o9 ] L7 y) f6 s
. `: f/ M1 Z: C4 i6 a% N; ]3 y! _- Y0 m/ d
3、如果P8=P16,则明显next[17]=8+1=9: ?9 v- K* N. ?+ y; H0 O
4、如果不相等,又若next[8]=4,则有以下关系
2 A% \/ D8 h9 B; @" i5 B3 w$ F& c9 q1 V
/ H! D/ B# W% ?4 Z; U" W: D又加上2的条件知( ^: \8 R# I4 t9 v- a; b- R
) X- x: S/ ^, I0 [' {' _) P" m& x ^
, W6 ? R9 l. m' r. v; J9 j' o主要是为了证明:" N( x1 ^5 D; ]3 H" A. m; M' O: E, b
* O; r8 M3 b( ^- L1 L8 k: ^
1 X, b* g! e- P+ N7 V9 x5 U5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推
8 A8 K) x& Q* o3 b& S6、若next[4]=2,则有以下关系& _: _- C* ` G" L
1 b! g G# L5 m, V4 g' W! {1 P+ W; U3 B) q, A! u6 _: k
7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!0 p" `) s* l: g: Z/ Y. c3 N
————————————————
3 _+ S V/ C! E) B9 \* \ q版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
2 N0 o4 l5 ^- m' g原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411% K/ h$ W" P5 F" r5 {
7 K4 y, ^* @' l0 c
! E6 s) h, M& g9 M* f+ W% J: e8 |( x& L/ k# u5 D
|
zan
|