- 在线时间
- 1630 小时
- 最后登录
- 2024-1-29
- 注册时间
- 2017-5-16
- 听众数
- 82
- 收听数
- 1
- 能力
- 120 分
- 体力
- 567276 点
- 威望
- 12 点
- 阅读权限
- 255
- 积分
- 175406
- 相册
- 1
- 日志
- 0
- 记录
- 0
- 帖子
- 5313
- 主题
- 5273
- 精华
- 3
- 分享
- 0
- 好友
- 163
TA的每日心情 | 开心 2021-8-11 17:59 |
|---|
签到天数: 17 天 [LV.4]偶尔看看III 网络挑战赛参赛者 网络挑战赛参赛者 - 自我介绍
- 本人女,毕业于内蒙古科技大学,担任文职专业,毕业专业英语。
 群组: 2018美赛大象算法课程 群组: 2018美赛护航培训课程 群组: 2019年 数学中国站长建 群组: 2019年数据分析师课程 群组: 2018年大象老师国赛优 |
KMP算法—终于全部弄懂了- G& {! y' K) T# _3 m
简介( u) d* w& y% q7 b# d2 b' Z/ }% ]
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
" w9 P B* u$ m) E2 e
~# h4 Z" U- D+ n/ ~8 E' c/ y/ b$ V: M, D
提取加速匹配的信息# @1 n4 A0 J3 ~8 t! E: f
上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是则呢样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!
^& c' Q; T- }7 U h- _; i 这种信息就是对于每模式串 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 }。
, ]- H0 E$ g- w8 y* F% h" N
' J; _( W! L3 g9 T" ^0 Z 加速信息,即数组 next 的提取是整个 KMP 算法中最核心的部分,弄懂了 next 的求解方法,也就弄懂了 KMP 算法的十之七八了,但是不巧的是这部分代码恰恰是最不容易弄懂的……
1 Q5 {8 l* E1 x5 R % I6 K- O$ }. u7 ~
先上代码
' N0 r3 U! R" u9 Y9 U) A3 P
1 ? |4 n. m8 q5 @# |) t( e3 R
1 k4 A# j) c5 X% o3 s, Jvoid Getnext(int next[],String t)2 O' X' n. Z9 R. A9 O' a
{
2 m4 j% W, ?; T; C int j=0,k=-1;! Q O2 N9 J$ H# h
next[0]=-1;
3 Q T6 c; q4 }+ F* l6 ?6 G0 t4 R while(j<t.length-1)
* j7 s3 b3 ?" ]8 F' g# [ {. d" K, r1 ?9 W8 I+ O$ U8 ~* c, s' c# R
if(k == -1 || t[j] == t[k]); q2 b: }. S: t. V
{
/ _ m% x9 z9 h p! ` j++;k++;
% M0 G0 \$ f4 Y' E+ p next[j] = k;
8 d8 q) q; y7 [% ^# l" j3 _3 D }+ @7 ~5 J# u8 Z5 Y: g
else k = next[k];//此语句是这段代码最反人类的地方,如果你一下子就能看懂,那么请允许我称呼你一声大神!
% R* b8 c" l" w( G. b }) q# J4 r8 y6 [# | s
}
# ^3 J6 E( D- u- H9 C4 D, U. T5 h& d! Q; K& T! Z& C: N6 L+ L/ L& j
ok,下面咱们分三种情况来讲 next 的求解过程5 A o4 @1 c: `' z$ E& s
# }( @) ]4 s3 ]9 ?/ m4 D0 k
: w! _. d6 O. U7 O* v
特殊情况2 l& b5 M& S3 R3 }! [1 A
当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。
_8 ]. I: C2 }+ T. ~( l4 d, ?" a7 K
3 ~- ]% V. F$ A B5 O! u& B0 o当 t[j] == t[k] 的情况
$ `! | X; z0 W. P' j0 t2 h举个栗子
7 Y7 Y; n3 Q8 E! C
S9 T" R8 Q4 N1 ]$ W% j+ Y
观察上图可知,当 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。
9 z! g! b' [, D& C& a2 t$ x. P
. m. o4 f6 u# J" |1 e: F9 ^( }$ N& b& V' j
当t[j] != t[k] 的情况
5 X( W' s) P8 }) w+ J关于这种情况,在代码中的描述就是“简单”的一句 k = next[k];。我当时看了之后,感觉有点蒙,于是就去翻《数据结构教程》。但是这本书里,对于这行代码的解释只有三个字:k 回退…!于是我从“有点蒙”的状态升级到了“很蒙蔽”的状态,我心想,k 回退?我当然知道这是 k 退回,但是它为什么要会退到 next[k] 的位置?为什么不是回退到k-1???巴拉巴拉巴拉…此处省略一万字。
* ~3 O) h! o* {0 ~
6 d- _+ o1 T9 I+ {
9 k% x, m! q1 j' o我绞尽脑汁,仍是不得其解。于是我就去问度娘…6 B0 b$ W6 U" b6 s% c( `% _
在我看了众多博客之后,终于有了一种拨云见日的感觉,看下图+ i7 Q2 ^' F1 F: i% i( E
) ]5 H! p) W L1 \ e7 r9 ~ 由第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。/ P# O3 C5 V* f0 A! Z
3 ^: H. C$ c* a4 f
: x4 X* r' G( B4 Z0 [7 h至此,算是把求解数组 next 的算法弄清楚了(其实是,终于把 k = next[k] 弄懂了…)
( |6 |8 w2 v* n
v+ p- \0 O6 [. x4 F- H
9 d0 p2 I& N; @) X% W/ @6 A$ T因为这个算法神奇难解之处就在k=next[k]这一处的理解上,网上解析的非常之多,有的就是例证,举例子按代码走流程,走出结果了,跟肉眼看的一致,就认为解释了为什么k=next[k];很少有看到解释的非常清楚的,或者有,但我没有仔细和耐心看下去。我一般扫一眼,就大概知道这个解析是否能说的通。仔细想了三天,搞的千转百折,山重水复,一头雾气缭绕的。搞懂以后又觉得确实简单,但是绕人,烧脑。! y, M* C) G* m; h& N8 h( _
" @6 W$ ~4 k" J7 @( Y9 [9 e8 S! }4 O6 o
再此特别感谢昵称为“sofu6”的博客园主,正是他的博客,让我这愚笨的脑袋瓜开窍了2 V5 I, Z: H9 F O
- p2 H" C% T( d& u1 e
6 n% a: j2 H( i4 g7 w/ d- P1 y* L) Y
KMP算法实现% S. ]$ |' H0 g& Q* D0 ]3 M/ ^: }
当你求出了 next 数组之后,KMP 算法就很轻易搞定了,下面我用三张图,让你明白 KMP 算法完成匹配的整个过程。
( F d: n' Z) h& P以目标串:s,指针为 i ;模式串:t 指针为 j ; 为例
, s3 {# L) `. J. Q" f
& A: m1 C- L! D! j/ Z/ }上图表示:“si-j ~ si-1” == “t 0 ~ t j-1”,s i != t j(前面都相等,但比较到 t j 时发现不相等了)且next[j] == k。5 q/ S& O9 p% R$ C) o
4 ~' e/ `$ l9 c v' I
根据 next 数组的定义得知 “t k ~ t j-1” == “t 0 ~ t k-1”,所以 “t 0 ~ t k-1” == “si-k ~ si-1”. e$ b1 z2 @6 j- Q- I: t# V& p' T
9 G4 C. b- K/ L6 ]2 U- ?& Z
将模式串右移,得到上图,这样就避免了目标穿的指针回溯。1 y/ @5 w/ Y9 G& L# Q) ]
# x, W, J/ n9 ~" ~, w
0 t% j" @. e* _5 F都明了之后就可以手写 KMP 的代码了
' u, W5 Z$ u: D; Y+ K) E/ L( v& F3 O. q# _9 J" f3 P: \
1 ?( E/ X7 a' h. F a6 \# U
int KMP(String s,String t)
& [+ {, R, u( u1 H: w' ?3 F{* ?0 R4 l% h" ~* B6 r( [5 T
int next[MaxSize],i=0;j=0;
9 Z& e# k, U1 y2 E Getnext(t,next);
9 D8 ^' I3 T* L while(i<s.length&&j<t.length)6 D/ U$ k' u- s" l0 ^9 p
{: `5 ~# H) S) u# r) n6 B' I
if(j==-1 || s==t[j])
: ^$ P# h% X# u* ~ {: h3 o/ M& Q# Z$ W$ v+ h" D: X3 J
i++;2 c- z0 j1 I1 i6 H+ R4 j
j++;
9 V2 p# E( J$ @. a O }8 b* E; D: j" N; A- c+ K. X
else j=next[j]; //j回退。。。
) \$ Z6 C: x' e. x' h8 d3 L }: J3 m" E) K @1 |! U* k
if(j>=t.length)
: h" r* ^* J- J4 C return (i-t.length); //匹配成功,返回子串的位置
3 N" q1 @9 S0 L( s" [% O$ I else
3 r6 G: X: x3 |- ` E return (-1); //没找到, Y; M7 s* }5 {1 J: Y8 J
}
2 I" G& z, \0 ?# n0 Y% q1 l1 ?1 I. w% o6 s, b
改进后的 next 求解方法/ f( c) o' E; _- `, \- k
先来看一下上面算法存在的缺陷:' R. R3 B0 N8 M5 _
- h& T! P: y' R' K( p( {4 N
显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]* Z3 m4 O; M) K% b$ I
" ?# [- W9 g) ~/ Y8 \ I2 }1 p
8 n8 o" O5 b$ T& `3 a- \2 w所以下一步我们应该是把j移动到第1个元素咯:6 B) E5 L* [0 r+ i/ O+ y( a
7 C* U/ G: I# ?& z( G不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。0 ^: C7 Q1 [# h) w) K; E
* j( G/ L9 A3 [: V
, V+ Q) K9 j; ^: W- X0 m6 ^显然,发生问题的原因在于t[j] == t[next[j]]。
5 i4 O' w; Q# \& P& t5 k X) K5 F: b; K/ B/ C# |. @! @- i
3 e& P3 P, Q5 a8 G. Q: B m所以我们需要谈价一个判断:
- c, ]+ i' U) X, X2 Y4 K6 l2 G
6 n2 G3 k, ]/ t% c9 U2 O+ j7 y
' m/ Z6 W7 A2 a" {, Tvoid Getnext(int next[],String t)
- |2 L- z% m P( u9 `{2 Q* {3 Z/ O' B. M
int j=0,k=-1;
0 V, ^1 z, ]1 u+ z next[0]=-1;
0 M! R0 ^/ x" g0 X+ \ while(j<t.length-1)3 Q5 M3 }: |& T- h# R! H, n
{: f7 ]2 x6 q1 V9 s: c2 S, o1 Q* ]
if(k == -1 || t[j] == t[k])& w: `8 f( d. U# S3 M
{
) n2 T# }4 {& d( V j++;k++;* h1 C3 P: W$ L5 Q
if(t[j]==t[k])//当两个字符相同时,就跳过
& v# k* ?1 P9 ~* y next[j] = next[k];) k: Z/ v* }. o- z9 D, ~2 w
else
0 N0 d! L4 K9 L next[j] = k;
& A; N0 C2 x% g }
. m" X2 Y! i8 e else k = next[k];
) V. e7 A2 |4 A7 D. V* M }
5 j# H/ X1 J8 F& |* S}
1 ~5 \ w8 g- h$ Q, ^# e0 k————————————————
! G% l" i* ~ u7 a9 I1 d版权声明:本文为CSDN博主「June·D」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。" p, B" ?6 T3 i+ N6 l- y4 V: S/ [8 f
原文链接:https://blog.csdn.net/dark_cy/article/details/88698736) c, v6 z7 o9 {) y" V
6 M1 P0 t/ M0 U3 D y8 x
& _9 `$ F/ T; w; \0 d, A
|
zan
|