- 在线时间
- 1630 小时
- 最后登录
- 2024-1-29
- 注册时间
- 2017-5-16
- 听众数
- 82
- 收听数
- 1
- 能力
- 120 分
- 体力
- 564720 点
- 威望
- 12 点
- 阅读权限
- 255
- 积分
- 174639
- 相册
- 1
- 日志
- 0
- 记录
- 0
- 帖子
- 5313
- 主题
- 5273
- 精华
- 3
- 分享
- 0
- 好友
- 163
TA的每日心情 | 开心 2021-8-11 17:59 |
|---|
签到天数: 17 天 [LV.4]偶尔看看III 网络挑战赛参赛者 网络挑战赛参赛者 - 自我介绍
- 本人女,毕业于内蒙古科技大学,担任文职专业,毕业专业英语。
 群组: 2018美赛大象算法课程 群组: 2018美赛护航培训课程 群组: 2019年 数学中国站长建 群组: 2019年数据分析师课程 群组: 2018年大象老师国赛优 |
KMP算法—终于全部弄懂了; ~2 S! e# z7 Q8 h1 B( T
简介( \6 B4 B& w, i, \0 b, t
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。, T$ }; I% ^* V8 ?0 c# C
, j$ G: j' J3 I) f' U2 l6 i; I. k4 G b% b9 j
提取加速匹配的信息8 Z9 z2 L$ ]) S* ]
上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!
; K* Y$ w. o* {$ w! K 这种信息就是对于每模式串 t 的每个元素 t j,都存在一个实数 k ,使得模式串 t 开头的 k 个字符(t 0 t 1…t k-1)依次与 t j 前面的 k(t j-k t j-k+1…t j-1,这里第一个字符 t j-k 最多从 t 1 开始,所以 k < j)个字符相同。如果这样的 k 有多个,则取最大的一个。模式串 t 中每个位置 j 的字符都有这种信息,采用 next 数组表示,即 next[ j ]=MAX{ k }。
9 T: j$ x" z5 C8 L / q @1 I, L F2 ~6 o* H8 N
加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……
# e) I7 Y# B0 k4 c2 m' V
7 M. u+ F% I% K- L+ X6 b先上代码
8 _# n) Q0 o- u* S# W' g8 ~5 ~& x
$ p9 k8 ]5 \- C$ h9 \9 \. d
void Getnext(int next[],String t)) g A j4 O% _& M4 c( j7 @
{/ C- `# R7 I6 H! t
int j=0,k=-1;, P# ?, g7 J, {9 \ g$ D$ o
next[0]=-1;
. i' @- L: q9 B6 I0 `1 t while(j<t.length-1)5 v7 i7 d! x, v& {, M% M/ y& ~% u
{* `% r! }) T Y
if(k == -1 || t[j] == t[k])
" i- a! I! E& Q9 [, v9 S- O& K {
; E$ b4 X$ Z5 T j++;k++;; C. b" M6 ~$ t6 D
next[j] = k;
9 u1 J7 w: H" O* g9 N% p. g }" p: _' {" p7 g: A. v$ }: w
else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!# m6 B/ `$ Q: B: q# v
}/ J: J" k1 _) w9 x2 G
}
3 r& t/ [& K4 ]! k
, _3 U- ~+ @* h) Q3 Jok,下面咱们分三种情况来讲 next 的求解过程3 P1 l h+ o! L$ h/ [& T
4 I% M& A$ o0 [8 g+ }. w& H7 F
) {% P& R! Q8 C0 N$ }" Y$ n' q
特殊情况
+ T" U( i# h7 m% Q0 g当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。) M; b, K. d8 ^9 o( y, B/ w
2 ?: J2 R. ?" s
, w7 c0 c( } _4 n: v当 t[j] == t[k] 的情况
" {) @9 U' t- h% L7 h举个栗子
) v- X9 d8 D3 d8 \" @
h# p4 s1 V% |% P
观察上图可知,当 t[j] == t[k] 时,必然有"t[0]…t[k-1]" == " t[j-k]…t[j-1]",此时的 k 即是相同子串的长度。因为有"t[0]…t[k-1]" == " t[j-k]…t[j-1]",且 t[j] == t[k],则有"t[0]…t[k]" == " t[j-k]…t[j]",这样也就得出了next[j+1]=k+1。
6 _1 Y- p% Z. ]( J6 w7 r
! h1 g6 }0 L. f+ v$ `( g; F: |4 Y$ A# A* m! v- J9 ~& d
当t[j] != t[k] 的情况
8 j. X. r# d( u; B: y7 e' {关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];。我当时看了之后,感觉有点蒙,于是就去翻《数据结构教程》。但是这本书里,对于这行代码的解释只有三个字:k 回退…!于是我从“有点蒙”的状态升级到了“很蒙蔽”的状态,我心想,k 回退?我当然知道这是 k 退回,但是它为什么要会退到 next[k] 的位置?为什么不是回退到k-1???巴拉巴拉巴拉…此处省略一万字。
3 n! ^# @6 Q9 f* t: L
. Z6 n7 R; b% \6 ~: c* t2 v ~7 ~$ w. S* Q8 J- ?% E
我绞尽脑汁,仍是不得其解。于是我就去问度娘…
6 l4 g& [& Z4 _$ P8 Q在我看了众多博客之后,终于有了一种拨云见日的感觉,看下图
9 P% Y2 V5 R# [+ x: ~: n p) J6 o- |
8 V F$ p$ S; _
由第2中情况可知,当 t[j] == t[k] 时,t[j+1] 的最大子串的长度为 k,即 next[j+1] = k+1。但是此时t[j] != t[k] 了,所以就有 next[j+1] < k,那么求 next[j+1] 就等同于求 t[j] 往前小于 k 个的字符(包括t[j],看上图蓝色框框)与 t[k] 前面的字符(绿色框框)的最长重合串,即 t[j-k+1] ~ t[j] 与 t[0] ~ t[k-1] 的最长重合串(这里所说“最长重合串”实不严谨,但你知道是符合 k 的子串就行…),那么就相当于求 next[k](只不过 t[k] 变成了 t[j],但是 next[k] 的值与 t[k] 无关)!!!。所以才有了这句 k = next[k],如果新的一轮循环(这时 k = next[k] ,j 不变)中 t[j] 依然不等于 t[k] ,则说明倒数第二大 t[0~next[k]-1] 也不行,那么 k 会继续被 next[k] 赋值(这就是所谓的 k 回退…),直到找到符合重合的子串或者 k == -1。
9 M! U- z+ \2 K- a/ F; A- R/ ~1 M8 J5 e
3 P. z( U! s5 L% h7 k$ d
至此,算是把求解数组 next 的算法弄清楚了(其实是,终于把 k = next[k] 弄懂了…)
/ {7 P& w. N9 d l+ _5 i R$ W m; v4 g! [" r
' z6 W5 s: ^, |: L0 `
因为这个算法神奇难解之处就在k=next[k]这一处的理解上,网上解析的非常之多,有的就是例证,举例子按代码走流程,走出结果了,跟肉眼看的一致,就认为解释了为什么k=next[k];很少有看到解释的非常清楚的,或者有,但我没有仔细和耐心看下去。我一般扫一眼,就大概知道这个解析是否能说的通。仔细想了三天,搞的千转百折,山重水复,一头雾气缭绕的。搞懂以后又觉得确实简单,但是绕人,烧脑。2 C/ p4 N7 W8 ^5 N ^, s
: N5 V0 _& \, v; ]2 C8 ?8 a
2 J _7 o g$ m/ Z( _& b再此特别感谢昵称为“sofu6”的博客园主,正是他的博客,让我这愚笨的脑袋瓜开窍了$ J7 @+ z. N% r% I2 l
. b& W1 D/ b( X O, R
: K$ ?# f, z$ ]KMP算法实现+ R* x2 c# j, L
当你求出了 next 数组之后,KMP 算法就很轻易搞定了,下面我用三张图,让你明白 KMP 算法完成匹配的整个过程。8 T; h1 Z4 H' ]
以目标串:s,指针为 i ;模式串:t 指针为 j ; 为例
8 v# N8 f* L6 K
' b4 E: D1 a% m( G2 x# E8 I
上图表示:“si-j ~ si-1” == “t 0 ~ t j-1”,s i != t j(前面都相等,但比较到 t j 时发现不相等了)且next[j] == k。# \; d& S& V, S( {: Z
3 i3 }* [7 J7 y/ M2 ?根据 next 数组的定义得知 “t k ~ t j-1” == “t 0 ~ t k-1”,所以 “t 0 ~ t k-1” == “si-k ~ si-1”& B4 y$ T1 N- Y5 l; f& q
% Z4 D) N9 }9 q7 m0 k$ h/ t2 c
将模式串右移,得到上图,这样就避免了目标穿的指针回溯。& H! w7 s Y) D) \$ A3 Z
t2 B7 R/ F9 L' N' w( Q- }
9 W0 P+ E2 c7 k7 H9 ?, x都明了之后就可以手写 KMP 的代码了" t6 z8 r/ M- k% e5 Y" n- n" w0 N
/ e: E. t H# d0 Y, K1 R# C* V7 b
. I& {# X6 h" J2 lint KMP(String s,String t)) x6 q1 k( K, G/ U; @& r
{' r" ?, [, Z* n: d: T
int next[MaxSize],i=0;j=0;: h2 \! P! @7 Q5 Z n
Getnext(t,next);
1 |, z/ L9 E* ? `+ @' C9 V; m while(i<s.length&&j<t.length)
7 H9 W2 D/ I. I {
; l; X6 x/ n5 e2 H% z4 E* a8 P9 f if(j==-1 || s==t[j])
9 D( w2 m7 u3 L, c4 } {) X' b h& B) I9 ^+ E s
i++;( d1 j' C) p* G" y
j++;1 R( c% d% w2 U- b/ E/ |& @, G$ r
}
2 D: v* }- R5 V8 H2 ~! K else j=next[j]; //j回退。。。1 I9 A9 Z! H/ F) c C3 K
}" N- x% }7 v/ T8 R6 Q1 b! J T
if(j>=t.length)0 V' @6 M; f3 p' _8 a
return (i-t.length); //匹配成功,返回子串的位置
( g1 w/ k6 p `! G' }+ ` else) _8 v- P: n; o' A" q
return (-1); //没找到
$ h) m6 r! @0 e+ q- L) {- }, q9 U}; p8 w+ |8 N. ]- t( F" B7 E; \
- N" }$ j a0 O$ f# ^: U改进后的 next 求解方法0 r# ?3 `: F5 ~, C5 t7 T
先来看一下上面算法存在的缺陷:3 W6 o# N) I/ P/ u3 s9 b
, L/ |( Q' ^- K7 R! V+ z3 I. D显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]# n3 p0 p0 k7 Y! I; I# v1 i! X
1 [: }0 b1 w; l, B6 e# T. w" T N" r5 k: k- E
所以下一步我们应该是把j移动到第1个元素咯:( ^) a2 z9 F. T9 M, [; a4 R0 m
# g* n" n, |) f d* g不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。
8 j1 c: W; ?8 h9 v: |" g+ I
( v: W M9 C# B" F8 G0 U& @. U) K3 e0 r5 ^; i2 F
显然,发生问题的原因在于t[j] == t[next[j]]。
+ H. Y" Q1 [! S# T' h3 H; ]
) q' |$ N& {% c0 h0 T
; [- |% b) Q2 l1 s% [所以我们需要谈价一个判断:! R4 ^, E, }/ F9 B: N1 @
. l4 x9 P6 `3 `7 z j+ l4 m% f
, Y6 Q% J# P R: Rvoid Getnext(int next[],String t)
7 S( G' X' O( H: d& e3 {3 x. c( P9 b{
' |3 P* }3 z+ k int j=0,k=-1;
! q) ~5 l/ B; O# P. }* S1 S next[0]=-1;9 r. z; m- U& E4 V0 \( w8 R) W
while(j<t.length-1)$ M7 V/ b* S# `, h4 K% k9 e; q
{
- b1 Y9 h, Q! S' H' t- M6 G( M if(k == -1 || t[j] == t[k])
1 ~* g5 R) d: o9 F( @- l, N. f {
; ~& _% q) K; @6 l9 _2 \6 M% [ j++;k++;
/ z8 c& ~+ ~: o7 L' F& s& j- Z. R if(t[j]==t[k])//当两个字符相同时,就跳过
- j+ P. @( A3 }1 A next[j] = next[k];6 O0 }7 J @/ ]5 j, p
else
+ @$ u$ p( W0 I! Z next[j] = k;
- @8 n* Q. S* v0 t1 I: g# v }
8 Z }# d+ N* F5 `7 q1 h; L else k = next[k];- r8 q. |3 y1 U* B& C; n$ C1 `
}4 e5 c. M# K3 D' ?: u' _* e
}
! f, v2 o# V, Q7 `1 J6 z+ I: E————————————————. b9 L) u/ C: }2 W
版权声明:本文为CSDN博主「June·D」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。& ~6 u; ]0 t+ Q$ C) h% C, z
原文链接:https://blog.csdn.net/dark_cy/article/details/88698736
7 Q4 [& s% ^) o, o% X: D
4 v9 k* a" o/ X# B$ B
0 {& \- G7 u. I0 T |
zan
|