在线时间 1630 小时 最后登录 2024-1-29 注册时间 2017-5-16 听众数 82 收听数 1 能力 120 分 体力 567262 点 威望 12 点 阅读权限 255 积分 175401 相册 1 日志 0 记录 0 帖子 5313 主题 5273 精华 3 分享 0 好友 163
TA的每日心情 开心 2021-8-11 17:59
签到天数: 17 天
[LV.4]偶尔看看III
网络挑战赛参赛者
网络挑战赛参赛者
自我介绍 本人女,毕业于内蒙古科技大学,担任文职专业,毕业专业英语。
群组 : 2018美赛大象算法课程
群组 : 2018美赛护航培训课程
群组 : 2019年 数学中国站长建
群组 : 2019年数据分析师课程
群组 : 2018年大象老师国赛优
(算法)通俗易懂的字符串匹配KMP算法及求next值算法 8 _. M( F# ^2 n. b. C
$ \8 y- m6 u$ d/ P1 u
大多数据结构课本中,串涉及的内容即串的模式匹配,需要掌握的是朴素算法、KMP算法及next值的求法。在考研备考中,参考严奶奶的教材,我也是在关于求next值的算法中卡了一下午时间,感觉挺有意思的,把一些思考的结果整理出来,与大家一起探讨。 / z' l) |5 ]; c4 t/ h
% _9 d4 }) @0 A$ O3 y; q3 l4 x) O 4 F% v1 f. [$ c/ a, |; C: u4 S; ~
本文的逻辑顺序为
2 z5 M/ c6 [: s( @" M9 _! Z 1、最基本的朴素算法
0 a% L9 n0 A7 P0 A 2、优化的KMP算法 8 M* U4 q0 N7 g/ @3 |
3、应算法需要定义的next值
" k) M8 I' P. I J* c5 D 4、手动写出较短串的next值的方法
. P0 N. z+ Z( u* {0 P 5、最难理解的、足足有5行的代码的求next值的算法 2 ^- P1 A3 j" r
所有铺垫为了最后的第5点,我觉得以这个逻辑下来,由果索因还是相对好理解的,下面写的很通俗,略显不专业… - J7 m' K, s: o5 m1 l" {6 v
- h9 A" ]* N$ y0 [- g, {
" v+ q: u9 N$ ?) a& C% m B
一、问题描述 0 S1 x, u4 z7 b) Z2 x
给定一个主串S及一个模式串P,判断模式串是否为主串的子串;若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0;如S=“abcd”,P=“bcd”,则返回2;S=“abcd”,P=“acb”,返回0。 ! T! b% }: f4 o9 Y
- b8 J$ b# d @, e, ^. p+ I- c
- l( }: e) K7 Z 二、朴素算法
0 _5 T) f7 Z; h/ `9 ^$ D& s) k 最简单的方法及一次遍历S与P。以S=“abcabaaaabaaacac”,P="abaabcac"为例,一张动图模拟朴素算法: ! Q/ v! J2 ~3 c- j3 `) h
$ c- j8 ^0 l7 V$ I6 ~" F " i4 {6 E! \/ X0 Y
: _$ ?) u5 w! t2 |/ K9 ~; I ' m/ D# y, d- G, h* `
这个算法简单,不多说,附上代码 , G0 V0 _) v. i& r2 W1 E6 m% a, s
/ H' y* Z: M2 n% E, P9 \' M- g
2 B1 s- D1 j0 Q& ~- }! q$ O" e #include<stdio.h> - x, y, t& P8 a, a
int Index_1(char s[],int sLen,char p[],int pLen){//s为主串,sLen为主串元素个数,p为模式串,pLen为模式串的个数
0 W' ?) q- N% Z if(sLen<pLen)return 0; + e, ^* ~. }. K: q1 _) t% ?
int i = 1,j = 1; ) l) `) j0 y6 [1 _
while(i<=sLen && j<=pLen){
+ z- q6 z8 k- g* ~ if(s==p[j]){i++;j++;} / L0 T8 }9 u( i# [$ z1 x
else{ . }: W4 }" U/ {7 Y! S V
i = i-j+2;
6 a1 S! W3 S8 r+ c* { j = 1; 0 v' M# x2 @: P1 \
} ( e( ]# ]+ Y, l5 \1 Q
}
( P9 q0 e% t# `' a4 x if(j>pLen) return i-pLen;
% L# Z3 R4 \0 e8 }+ u return 0; ( c) s% h, L: A" o
}
6 I) u0 n: k( f6 a1 d2 F void main(){ 0 A8 L4 k. n$ Y. |( T/ a
char s[]={' ','a','b','c','a','b','a','a','a','a','b','a','a','b','c','a','c'};//从序号1开始存 5 j% O6 `6 Y0 T) B7 g
char p[]={' ','a','b','a','a','b','c','a','c'}; " d8 K7 o" K# [) I6 f( q
int sLen = sizeof(s)/sizeof(char)-1; 5 t$ z. ?# H4 L* i# m1 P6 e
int pLen = sizeof(p)/sizeof(char)-1; 9 m6 T. T" W( ?& x
printf("%d",Index_1(s,sLen,p,pLen)); # ~) i- Y' I% Y" g* R
}
1 w5 y- p( A, [! q* C 1
- f9 G- ]' W- y0 {" F. n* A( u2 ] 2 ) S$ Z8 _1 y& n+ {+ F: ^ z8 w7 P/ p
3 5 M2 o, z# d# b# D, G- r* j
4 . a0 A$ h/ y" u$ {) L
5 $ z" N Q( `# O4 l) H2 V& J; R+ `
6
: z8 j+ `1 }0 z* D9 w 7
$ n& n8 [% t$ }6 o8 G7 @9 h9 ` 8
' L& C b9 h7 z8 }2 T% L4 w 9
* q( ~" V' `) J8 V4 G" t 10
, |$ L4 B+ I* } 11
1 m& q8 g" O' Y5 T+ J/ n" \; F5 f 12 - n; s8 u7 M0 O+ s; q/ V( `2 L( K+ @
13 5 ^1 F+ j1 E8 Z- [% g! \0 w1 g
14 ) `3 G" O) ^; t: ?8 e
15 : l( j$ @! S1 J+ |
16
; ^' P& |5 _- D& N2 I, F0 U 17 e2 j' L9 U( k& Y
18
5 J( K' w! j. k' J" C/ } 19
) d" U5 W a8 {. Y" P$ g 20 1 ?4 G+ `1 M% \, d8 @4 \- ^8 l! V
21 8 p- f' ^7 Z. [0 n3 f8 [
三、改进的算法——KMP算法
2 {" k6 _+ U! ^" r 朴素算法理解简单,但两个串都有依次遍历,时间复杂度为O(n*m),效率不高。由此有了KMP算法。 r7 B0 ~1 o1 _+ z6 X
一般的,在一次匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。
' t: V) M9 Z: l9 p- _ 朴素算法中,P的第j位失配,默认的把P串后移一位。
& n7 D9 u* c4 L2 i( e# @ 但在前一轮的比较中,我们已经知道了P的前(j-1)位与S中间对应的某(j-1)个元素已经匹配成功了。这就意味着,在一轮的尝试匹配中,我们get到了主串的部分内容,我们能否利用这些内容,让P多移几位(我认为这就是KMP算法最根本的东西),减少遍历的趟数呢?答案是肯定的。再看下面改进后的动图: . z7 N8 d( t; V6 C+ _+ [0 y; i2 V5 k
% v5 n1 C& u6 Q0 e/ e
& b: [, y }; r) \2 b+ \$ f8 l
% g7 B7 _ R8 B
; b# d& |! F9 H7 U$ @4 X9 e$ t 这个模拟过程即KMP算法,若没有看明白,继续往下看相应的解释,理解需要把P多移几位,然后回头再看一遍这个图就很明了了。 9 Q4 n# p% ^" X- N$ A
8 D$ M$ \2 d6 X) ?$ s
* q" z! k v: E( y+ f+ P
相比朴素算法:
0 c. ^0 [" `0 M( X* P o 朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m) T# |% b& E* h
KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高
3 A9 d5 W! {' ^) U9 u% Z) G) j$ b
1 F- \* R; J# Z7 @) B' o . e: P+ A) b* i" \) P6 q7 G0 X. \
而这“定位到某个数”,这个数就是接下来引入的next值。(实际上也就是P往后移多少位,换一种说法罢了:从上图中也可以看出,失配时固定i不变,令S与P[某个数]对齐,实际上是P右移几位的另一种表达,只有为什么这么表达,当然是因为程序好写。) ' \8 P6 |3 j* J- B! K" ~
! @# @4 i. \& o9 u; Q
- Y( [) e: r _& l& u5 j+ T+ [ 开——始——划——重——点!(图对逻辑关系比较好理解,但i和j的关系对后面求next的算法好理解!) : V& I) }, _4 i! a+ _
|1 F/ ^; {2 k) u( [" ~, d) F
- k2 y3 J7 h/ F. } n# S; k 比如,Pj处失配,绿色的是Pj,则我们可以确定P1…Pj-1是与Si…Si+j-2相对应的位置一一相等的 4 P; R5 R. d' o$ y2 U2 L3 O
/ d( Q. }+ e0 |+ U s. K 6 V. L5 Z! S/ U8 T
$ A. [; A+ p- x4 W5 c4 R2 E
假设P1…Pj-1中,P1…Pk-1与Pj-k+1…Pj-1是一一相等的,为了下面说的清楚,我们把这种关系叫做“首尾重合” ) n. ]/ p4 y9 k6 E3 T" I: d! `
5 Z' T/ X6 {1 p, d* \+ ~0 S
1 o1 L7 r0 J: r6 A4 Y" u
4 \9 b1 V" I5 \. R4 F 6 J0 g1 u, I6 q( b" m, `; d
那么可以推出,P1…Pk-1与Si…Si+j-2
! S9 Z& \8 \' O5 a/ d ' ~; Z9 j3 X2 T& {# S2 A
7 B: e9 c! X5 f) G4 T7 |! }5 |
) @* X4 k& t: {- y; Z8 K
. E2 A8 D2 }9 c0 b5 z
显然,接下来要做的就是把模式串右移了,移到哪里就不用多说了:
7 w) P/ q3 }. [3 Q8 D! o
! `5 r+ G$ u* [: N+ u
' ^5 {3 P0 B' [$ n" }- o
4 ^& l+ `% _1 e' p5 T 7 ?* H# H7 Q$ a( m: G2 S
为了表示下一轮比较j定位的地方,我们将其定义为next[j],next[j]就是第j个元素前j-1个元素首尾重合部分个数加一,当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值,原因挺好理解的,可以想个例子模拟一下,会完美跳过正确结果。在上图中就是绿色元素的next值为蓝色元素的序号。也即,对于字符串P,next[8]=4。如此,再看一下上面的动图是不是清楚了不少。 + `/ S9 X# b. ~2 M/ x- M3 E; y
# e# [( g: w/ J7 D
4 W1 m' j# T. a6 V
最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好啦!接下来就是怎么确定next值了。
3 A; \" I0 X( J" Q
% L4 W3 K+ R0 C) `' v1 K0 X
m* ]; i0 b, z% ] 四、手动写出一个串的next值 ) Y$ D q f& U9 @. m
我们规定任何一个串,next[1]=0。(不用next[0],与串的所有对应),仍是一张动图搞定问题:
9 D k4 x9 b) p. R1 y* o1 H* z % |7 V0 P) r2 S- I
' O2 j9 Q. d/ `/ x2 u 这个扫一眼就能依次写出,会了这个方法,应付个期末考试没问题了。
9 S% S5 R$ z4 O9 _) }/ A
! _* c. A. O, x. x3 d* R# g
. o; Z/ Y- j, A' e 通过把next值“看”出来,我们再来分析next值,这就很容易得到超级有名的公式了,这个式子对后面的算法理解很重要!所以先要看懂这个式子,如果上面的内容通下来了,这个应该很容易看懂了:
" {8 q* g' |' E9 c: X
: w; ?, T7 o6 _2 Q8 B$ W
* ` K) G) Y5 D- Y0 o" h) g
2 c1 T! F/ t% ]; c. S' U8 Y
6 Q: Y l& j9 ~0 [# n1 b 五、求next的算法
& Z+ H9 r, Y, ^2 | 终于到了最后了~短的串的next值我们可以“看”出来,但长的串就需要借助程序了,具体算法刚接触的时候确实不容易理解,但给我的体验,把上面的内容写完,现在感觉简简单单了…先附上程序再做解释,(终于到了传说中的整整5行代码让我整理了一下午)。
1 ]$ @$ F8 ~4 R2 O* T' O) m 0 d$ ^: \! \* N
; \- V6 _. I/ o3 K
int GetNext(char ch[],int cLen,int next[]){//cLen为串ch的长度 + V K' |4 ~0 C
next[1] = 0; 3 g& E8 b* ?: g
int i = 1,j = 0; 7 V! l, p {4 J9 ^7 _
while(i<=cLen){ - ~4 s: P2 {( [
if(j==0||ch==ch[j]) next[++i] = ++j;
7 w# W0 v2 E1 H7 H6 q( f) S" ] else j = next[j]; / c4 {/ N3 B) q" F% H
}
- c" |& w6 i2 b$ y. T+ R* m$ g# @ }
* E- x+ r- R7 H2 A * f# b. L2 l1 S0 G
还是先由一般再推优化: 9 F$ l: O5 p, i
直接求next[j+1](至于为什么是j+1,是为了和下面的对应) * Q# N1 _. `$ |6 ?7 d
根据之前的分析,next[j+1]的值为pj+1的前j个元素的收尾重合的最大个数加一。即需要满足两个条件,把它的值一步步“检验”出来。一是“个数最多”的,因此要从可能的最大值开始验;二是“首尾重合”,因此要一一对应验是否相等。 ' O& L9 n# k0 }+ V& m8 M1 n
不难理解,next[j+1]的最大值为j,所有我们从next[j+1]=j开始“验证”。有以下优先判断顺序:
1 ~- s) n2 p/ N6 z if(P1…Pj-1 == P2…Pj) => next[j+1]=j
9 g8 h: P* Y/ h% J8 n% q8 W: K else if(P1…Pj-2 == P3…Pj) =>next[j+1]=j-1 4 W* \ V8 h9 i7 O; A5 U9 p. d5 [- X
else if(P1…Pj-3 == P4…Pj) =>next[j+1]=j-2 # [' b) Z7 w% {! c
… 9 n, X& {$ V6 P3 m$ S! U k6 C1 W
… 8 N/ W# |' E$ G
…
& l8 N ?7 M3 z else if(P1P2 == Pj-1Pj) => next[j+1]=3 ) B% ?3 j3 o. w9 d/ R- `8 J
else if(P1 == Pj-1) => next[j+1]=2
; E2 B/ N7 P- [- o# |$ h else if(P1 != Pj-1) => next[j+1]=1
1 l+ i& F3 e4 w% a2 f$ E+ Z7 P 每次前去尾1个,后掐头1个,直至得到next[j+1]
. ]9 R# G; l, r$ E7 ~ ) C4 F9 R9 K$ x9 a: `
' J- Z/ |2 W/ g) ]: {4 S8 R, t
再进一步想,next值是一个“工具”,我们单独的求next[j+1]是完全没有意义的,就是说要求next就要把所有j的next求出来。所有一般的,我们都是已知前j个元素的next值,求next[j+1],以此递推下去,求完整的next数组。 # ~! O+ u r, A* j6 R. c
但是,上面的思考过程还是最根本的。所以问题变为两个:知道前j个元素的next的情况下,
1 P' E2 t1 u/ ~" R O% R2 ` ①next[j+1]的可能的最大值是多少(即从哪开始验证)
: y6 L0 V. I; Z5 l7 X5 j( b ②某一步验证失败后,需要“前去尾几个,后掐头几个?”(即本次验证失败后,再验证哪个值) ' w% J ]3 E( P
看一下的分析: & H) B9 j9 b3 c" m; d. o
1 i7 _8 F6 A; o+ ?
/ u6 y: L. k. t( b
1、next[j+1]的最大值为next[j]+1。
& b- ~& ~5 m2 |6 U$ ~) W0 C 因为: ; I4 @7 E% E5 A" t
假设next[j]=k1,则可以说明P1…Pk1-1=Pj-k1+1…Pj-1,且这是前j个元素最大的首尾重合序列。
# S, p. V" e" R3 j) q9 j6 o 如果Pk1=Pj,那么P1…Pk1-1PK=Pj-k1+1…Pj-1Pj,那么k+1这也是前j+1个元素的最大首尾重合序列,也即next[j+1]的值为k1+1
4 D k) P# z$ E, I6 X9 B 2、如果Pk1≠Pj,那么next[j+1]可能的次大值为next[next[j]]+1,以此类推即可高效求出next[j+1] ) \" @0 _5 g8 j; x6 k. S! Q( b
这里不好解释,直接看下面的流程分析及图解
! ]$ E* n3 ^( V& x. R8 z 8 |4 d" ?# ^% |; N% D3 N( t6 R
+ E( I; R/ c+ x4 v0 I( H0 B6 F% l 开——始——划——重——点!
8 c2 m O0 i9 u2 y, x 从头走一遍流程 * m) O0 ?& d# w$ B& V# j; O
①求next[j+1],设值为m
, z- W z, \4 E# ` ②已知next[j]=k1,则有P1…Pk1-1 = Pj-k1+1…Pj-1 Y" t8 l- h0 @; {( e! O5 p. ^
③如果Pk1=Pj,则P1…Pk1-1PK = Pj-k1+1…Pj-1Pj,则next[j+1]=k1+1,否则 . T& W% b1 ?- r0 X0 h7 s3 ?1 S* g
④已知next[k1]=k2,则有P1…Pk2-1 = Pk1-k2+1…Pk1-1 ! T- Y+ |* j R
⑤第二第三步联合得到: 4 }* e6 h9 n* @* |/ [% }
P1…Pk2-1 = Pk1-k2+1…Pk1-1 = Pj-k1+1…Pk2-k1+j-1 = Pj-k2+1…Pj-1 即四段重合 / K T' K5 o0 S2 c
⑥这时候,再判断如果Pk2=Pj,则P1…Pk2-1P~k2 = Pj-k2+1…Pj-1Pj,则next[j+1]=k2+1;否则再取next[k2]=k3…以此类推
- w: X0 j- l, }; `9 \2 Y9 I
" P$ H# j. k; Q+ ~
3 l& J0 L/ y: [5 o$ W) ?" f3 F$ n 上面几步,耐心看下来,结合那个式子很容易看懂。最后,再加一个图的模拟帮助理解:
# N6 _4 L3 Y: F3 m: b 1、要求next[k+1] 其中k+1=17 * h/ Y. ]: J- }+ P* p
3 j- U* ^) x" Q5 z* y
/ N7 @# H. `( S+ [9 Z* `( J6 \ 2、已知next[16]=8,则元素有以下关系: 2 I/ `$ d0 o- ~, r
( O2 u, B! j4 G6 u" U- E. ? + Q3 L- S1 G) {" @
3、如果P8=P16,则明显next[17]=8+1=9 0 V0 I9 H2 ^& Q; d# e1 z0 e: C
4、如果不相等,又若next[8]=4,则有以下关系 ( Q& P, V3 F- \+ _3 x/ W
* ?$ e3 R; s" H9 S1 Z# X
- R% v* P1 ]; {/ a0 d7 i3 F 又加上2的条件知
$ e# M8 K' U7 [6 `1 A. W " @- S- U& j# s3 b& }
: x' U" Z) l3 Y/ U$ b- h/ L
主要是为了证明:
* p' i2 F9 R+ ~( W9 M. W
: P7 q$ v$ Z2 N; Z8 a8 A3 [ M" n
1 ]' q6 F; N! [% L 5、现在在判断,如果P16=P4则next[17]=4+1=5,否则,在继续递推 5 C/ M# D0 r: h, ~# X' b, O
6、若next[4]=2,则有以下关系
6 \3 f7 H4 t2 g1 f
2 \, M7 }, ~* v3 U/ |
6 }' _+ f3 B X- V+ S; ]* i$ @ 7、若P16=P2,则next[17]=2+1=3;否则继续取next[2]=1、next[1]=0;遇到0时还没出结果,则递推结束,此时next[17]=1。最后,再返回看那5行算法,应该很容易明白了!
( V) B' D7 D. v# Y/ u( e% f ————————————————
6 q( i! l! M7 Y 版权声明:本文为CSDN博主「Sirm23333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 : i3 j c5 t; l
原文链接:https://blog.csdn.net/qq_37969433/article/details/82947411 1 P" s4 l" t1 G9 \* y
) B; ]1 W4 E( C) c
/ r5 |/ { i" o0 i ; J$ ~& R. G# s) _
zan