- 在线时间
- 1630 小时
- 最后登录
- 2024-1-29
- 注册时间
- 2017-5-16
- 听众数
- 82
- 收听数
- 1
- 能力
- 120 分
- 体力
- 557689 点
- 威望
- 12 点
- 阅读权限
- 255
- 积分
- 172680
- 相册
- 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值算法4 ^# A" _. q5 s' {. Q2 l
8 h& N! I$ V8 U* R
大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。: s3 [) `) O8 I- U7 \7 Q8 N
! n; r$ z, `3 N% w
) e2 Q' |: I: D* y; M+ ]本文的逻辑顺序为
- l% d. o# y/ r& F2 v/ K4 M0 H1、最基本的朴素算法
* C( W* p5 j" C7 F) k2、优化的KMP算法
# b4 f$ A, o3 y/ z) p3、应算法需要定义的next值' u: w2 a1 a* Q+ c4 B/ Q
4、手动写出较短串的next值的方法
2 a0 X; \4 w1 l2 e5、最难理解的、足足有5行的代码的求next值的算法) j' }9 R8 j. T. d T6 `, x
所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业…
% D. H9 R k! O/ L4 |, N5 B9 [
! ^+ F2 s3 Q N$ Z/ c9 m/ Z" k8 H6 b/ ]2 R
一、问题描述! z& h$ Y. Y4 R4 `9 g7 `8 H& }- ~
给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。' C9 C* l' h5 c- t- H7 ]/ Y3 n
4 k+ a. V H& V# V' w* ?3 f, y7 }3 J( P3 k, O
二、朴素算法
! |0 r' t# ?6 }) q最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法:
1 ?& Q2 }- @# [
% i3 u4 t7 G( P1 U4 x' n
0 M/ d7 l* |- h1 \# X+ |
7 y3 J+ l+ ~& e2 H$ o( L
4 B* I/ e6 _# G$ X' R
这个算法简单,不多说,附上代码
$ C1 v& Y. ]2 Q
" H- `( C; i; r6 i( w6 b; V) o/ Y9 _( S, j+ R
#include<stdio.h>* M' ~' n6 A; i1 E( J9 O& t' O$ u
int Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数
" o3 |. ~3 [6 a4 j8 R7 X if(sLen<pLen)return 0;
; I% i6 W$ [+ j int i = 1,j = 1;
0 h7 G0 S u* p3 R, ?6 A! ~ while(i<=sLen && j<=pLen){8 t, g5 \2 s; \5 V/ s" I
if(s==p[j]){i++;j++;}5 p/ K4 n W" u( g) N
else{
0 T; Y+ ^1 Q8 ^* \ i = i-j+2;0 b U# f5 ~$ S2 t. i: |8 o
j = 1;
@# ~; n J( L1 F$ K& ~; U# w& N }
- A/ p% I) D' }2 n0 j }0 W8 p$ G4 U$ ~( G/ [) M
if(j>pLen) return i-pLen;
' l( Z9 i7 b3 h: _% Z3 i return 0;
, q$ h9 O4 M+ i L* j; {& D5 `7 N6 l}1 h; u" ^$ i( u
void main(){
2 ?7 N. [, U( f+ p1 v6 a char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存1 {$ v0 o- ?' y! a; B
char p[]={' ','a','b','a','a','b','c','a','c'};# j+ u5 y% B5 V3 e( b" O5 t1 n* S7 y
int sLen = sizeof(s)/sizeof(char)-1;
6 i9 o5 X, a- g4 f- u. o' }0 z int pLen = sizeof(p)/sizeof(char)-1; H! G! ]. s3 @
printf("%d",Index_1(s,sLen,p,pLen));& `. h* b* e! Y& Q1 H
}: z {+ W! B. A
1
& W# L. c" \* m2
8 ^% u9 ~- A( J3 z# p+ l0 S3& o' M" M& {3 ?. u
4/ C) N( Q" l: t0 m$ Z% ?
54 I4 f/ F3 V6 G: f' G4 q, m* S
6- e+ D: x; [8 @% G% I0 B# n$ A
72 }( E( h9 r3 O# ]% U$ Y# {0 s
8' B. ]: ^3 n* q0 z0 b
93 h% e7 q6 U. V; X% w! ~
10. x$ v( U0 Q3 s9 ]: S0 h
110 r7 [$ k2 M+ x$ m6 M
12
7 ], a# u) `8 n13! j4 q/ t6 J# j. |9 e
14* R- m, |, t" C1 H
15
. O' ?% w4 z; z) |7 Z16( e6 l, @9 |0 i0 k8 L% c& `8 u* I# d
17
* Q0 E9 f/ O' a. u; z' x187 |, n$ d+ v6 h8 M! I+ L8 l; l
19
: _: r4 e' }9 c4 @8 ?20, W+ U* E' y$ Q& v; ?& y7 p q
21, f& U9 R/ n& j/ p& H1 [1 y
三、改进的算法——KMP算法
! z# o! E( l' Q/ j3 U0 K% t朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。& m$ Y0 a; [' c8 t
一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。& K. O, j) \9 |' X) G" w
朴素算法中,P的第j位失配,默认的把P串后移一位。
( b' D7 S+ i; J- K) m* G7 Z但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图:
g R* @, `7 @# \
3 k6 s/ |; |% O/ m) L( q/ [+ |
\0 \0 M( x0 o/ a6 B9 ^5 s x+ R0 }, f6 V
7 j! V2 Y5 l3 F0 b
这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。
2 P$ L7 C1 I/ }) o- {
" M( T7 M8 [/ ]- G7 J
# ?% Y5 n) i& D4 H! T相比朴素算法:
0 D/ W* I& B* ^9 \! M朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m): M% L M8 H% j8 I5 _( ^
KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高9 ~0 T0 c% z) g
J$ U8 d1 c! m% |$ D3 D
1 [: V- H) k0 k. E0 F而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。)7 b/ P2 z2 K% c$ L. @& Z# _
/ @7 [+ r: w/ {5 V1 S* K
. g1 H; k9 N3 [1 ^
开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!)5 j9 S$ f- k: T+ \; G2 k+ J1 _
1 O- O( S. {) z6 V) l3 M; V$ U+ p/ Z0 A' g7 Q& @( E/ F
比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的
3 ~9 Z5 v" X. ]0 }& r; n
* ]9 E- E9 B9 c4 n9 b& g& [- v) _# T+ B6 B6 e. Q/ c$ v4 B
7 R, f0 s/ J& Z. Q6 s/ x
假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合”
9 A) u3 K% y' X3 [
& d# @3 L* n& b1 r
* D" i% G+ T9 D- L& L7 k/ }% X' u0 _8 S* C0 P7 f: k
% x2 D/ D2 t* x3 _8 g
那么可以推出,P1…Pk-1与Si…Si+j-2; j+ V0 ^. m; p- S: W# g, K/ Z
) Y2 D0 C% z$ Q5 L: {- F7 q# F5 b
: E" H$ v/ S' K6 q0 n
) u. S9 _) E3 e# o, G
- F, s/ Z( z5 z5 M' [: n% p显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:5 F; u9 m. {. z, @& R: ^3 k3 n
/ a3 h+ y$ q7 ]
% N+ K& { C& ^- F8 _: V& K. n
) M: h% \% U7 h0 m1 d" h' [
{4 N0 G8 H" Y为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。
$ J6 o, D- {# d3 y6 l5 j& t: D* @, T: c; ]6 |
: P. a( |; m I& C最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
8 w" E& o3 T& I M2 A4 w1 B6 n) E4 ~: q, t
) k* z0 L( J/ b& _1 p% s6 }# C四、手动写出一个串的next值
6 ~5 ] H" H: P/ I4 @我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:
9 d1 Q9 i! \5 ^3 S- ~) I/ m6 h7 `" L. X) o7 }0 d
; ]5 G# N- i% a4 b: f7 d4 t这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。
/ N1 m+ c- s8 j; d
) }: p( B/ n0 m+ o" ^- f0 _
2 d U1 L5 U+ A& j K+ `4 t通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:/ Q; H% n2 R+ Y: U! q2 j2 v! k
4 j9 l. L% A% Z" o& H
9 D3 C- S6 E. B4 F6 T& h! w
& S& Q7 D' b6 S6 a$ Q
% F; r/ V. t$ I# c* v) I Q: K j( ~五、求next的算法
. Z$ u# K# W8 V+ W1 K. u5 }) }终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。9 [7 c# ?9 a" W! ?% L* f @6 E
6 l H1 G9 Z* Y6 }+ F' `+ ~
, Y' r& g8 ?$ V5 S1 Q5 c+ e8 T1 r
int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度3 w% J: F4 A" }0 o1 E& T: s
next[1] = 0;
+ [8 g9 h+ f! m! ?# O4 J int i = 1,j = 0;( p% Y8 _& N- J2 Z* c; m. q- K
while(i<=cLen){
% j; d% h! o3 ?) @- k if(j==0||ch==ch[j]) next[++i] = ++j;7 s2 y0 Y: \& z
else j = next[j];
8 _; l% K/ @3 X }
6 Z3 S. n8 j" j3 i% |}
& X, L! f9 j8 Z1 B' y
: c' @2 t3 i' t+ V; N5 c还是先由一般再推优化:
: e# y& E3 l% m2 p* K直接求next[j+1](至于为什么是j+1,是为了和下面的对应) R1 n; w" H7 t
根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。
w3 J( I1 Z! F+ m1 v$ m+ ?不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:/ ~) M8 u$ L% ^4 b) b
if(P1…Pj-1 == P2…Pj) => next[j+1]=j: H" h- l: j) [" l( _
else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1
+ |9 Y, } }' W, F) R* a; Belse if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-21 Q4 A% m4 h7 u, j% l
…8 x: A( {; c9 K( S' I; T$ u
…+ y( f4 Y' F# D2 f* Z% l
…! U/ J' x7 E8 B D" i
else if(P1P2 == Pj-1Pj) => next[j+1]=37 e8 j* o7 {$ @, l9 h' i
else if(P1 == Pj-1) => next[j+1]=2
- \7 o! ^! R7 x. o' e6 ~else if(P1 != Pj-1) => next[j+1]=10 e, a* n; r8 C {
每次前去尾1个,后掐头1个,直至得到next[j+1]/ _# ]3 j; F5 W; a
+ O1 r6 ]) W& w. W2 z1 r
( g) l# O% V% I9 Q& G0 ?再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。
4 x1 S) {3 z; L5 R; F但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,0 Y: b6 ]( W6 J8 h( I+ b6 ~
①next[j+1]的可能的最大值是多少(即从哪开始验证)
& e2 [! t% H) Z( W+ \8 A( L②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值)4 a e: E$ S4 @. [# F
看一下的分析:
+ z C6 r; I3 E, U1 a
7 n% {; u( {- z, D' W& m: T: t8 V$ V8 d, h2 S8 s* ^2 ]
1、next[j+1]的最大值为next[j]+1。( a: c5 {/ ?, w* g! e
因为:
, R/ x. a2 K1 j. x假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。2 A2 o* }0 ?3 P' | F$ O& |
如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1
) _9 M1 C2 L0 ~" U( e, \! k3 T2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1]1 k0 \! M% Q, d% |$ `2 L
这里不好解释,直接看下面的流程分析及图解" r' P' r8 Q1 w: F- j# c6 O z
0 Z l" y8 ] c, H4 o9 p
- Q- v2 f$ i, F5 J$ d# L' ]开——始——划——重——点!+ K& r. R& H" `1 @& W4 U2 A
从头走一遍流程: d) @# q, j$ F- ~3 C' z3 V7 L; L
①求next[j+1],设值为m' O6 x, j/ f( a' y6 z2 H# K: U1 \
②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1, ^9 z' C/ G9 K6 ]2 [; w; y
③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则
- e; b+ j9 }0 v. i; V* P# U3 j④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1% L* R. P# z# e: B7 G2 D9 W
⑤第二第三步联合得到:- i5 V$ m) L& f- ^: f% H/ l" f! j
P1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合/ ]) P! J0 Z# m& W( Q i8 Z
⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
5 q0 w0 a! u$ D+ Q/ N+ j- X6 | H7 ?" ?% _# r' n
& u7 e% _7 f" s上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
1 Z, B# W) [' J( P' {: J. j+ X1、要求next[k+1] 其中k+1=17
" u5 d% T5 b* o5 M
! b1 c6 Y2 ~- K( B" c
; l! v& I* o' C$ r( m' t
2、已知next[16]=8,则元素有以下关系:
3 j$ e" K3 n: _& w
$ N' S/ ^; B3 A! F2 H: R
, _+ h) O9 ~, B3、如果P8=P16,则明显next[17]=8+1=9
. x: M: R+ _3 g/ Q, U+ J4、如果不相等,又若next[8]=4,则有以下关系2 p# q1 {7 o( s
4 o; H! h6 y0 S! T) I# h* O
# n8 s% l+ @9 m$ }8 t& t1 ~
又加上2的条件知( V6 A. M) F1 f2 G5 X; [
. g7 G1 b# }$ Y) i8 d
' {- ^+ |/ M& x6 M主要是为了证明:4 z/ P0 ^* F! v q5 c2 Z9 r7 p& c
+ D+ t! W; f T+ }
6 K# ?4 F9 C [5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推
' b& |+ k! r2 d" [: ]4 `6、若next[4]=2,则有以下关系+ g' K- L; V# p7 k. e. T; f
( }7 o& H `5 L3 W
5 t# T: n3 J5 Z# \5 x7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!
/ Y% e: z9 A- D& \4 C————————————————2 ]+ {+ }1 \ V2 J/ A. I
版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。6 d7 N" m: T* P1 I" I. H; _! k, C5 P
原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411
4 ^! t/ B* h) T' u/ _+ F3 f& n' b6 x1 S& ~
8 b \0 X, e# O" q( I1 z
( _- E8 x* z7 `7 w% `/ C, N |
zan
|