22
33<p align =' center ' >
44<a href =" https://github.com/labuladong/fucking-algorithm " target =" view_window " ><img alt =" GitHub " src =" https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub " ></a >
5- <a href =" https://appktavsiei5995.pc.xiaoe-tech.com/index " target =" _blank " ><img class =" my_header_icon " src =" https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat " ></a >
5+ <a href =" https://labuladong.online/algo/ " target =" _blank " ><img class =" my_header_icon " src =" https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat " ></a >
66<
a href =
" https://www.zhihu.com/people/labuladong " ><
img src =
" https://img.shields.io/badge/%E7%9F%A5%E4%B9%[email protected] ?style=flat-square&logo=Zhihu " ></
a >
77<
a href =
" https://space.bilibili.com/14089380 " ><
img src =
" https://img.shields.io/badge/B站[email protected] ?style=flat-square&logo=Bilibili " ></
a >
88</p >
99
10- ![ ] ( https://labuladong.github.io/pictures /souyisou1.png )
10+ ![ ] ( https://labuladong.online/algo/images /souyisou1.png )
1111
12- ** 通知:[ 数据结构精品课 ] ( https://aep.h5.xeknow.com/s/1XJHEO ) 和 [ 递归算法专题课 ] ( https://aep.xet.tech/s/3YGcq3 ) 限时附赠网站会员, [ 新版刷题打卡挑战 ] ( https:// labuladong.gitee.io /algo/challenge/ ) 上线! 另外,建议你在我的 [ 网站] ( https://labuladong.github.io /algo/ ) 学习文章,体验更好。**
12+ ** 通知:[ 新版网站会员 ] ( https://labuladong.online /algo/intro/site-vip/ ) 即将涨价;已支持老用户续费 ~ 另外,建议你在我的 [ 网站] ( https://labuladong.online /algo/ ) 学习文章,体验更好。**
1313
1414
1515
2121
2222** -----------**
2323
24- > tip:阅读本文之前,建议你先学习一下另一种字符串匹配算法:[ Rabin Karp 字符匹配算法] ( https://labuladong.github.io/article/fname.html?fname=rabinkarp ) 。
24+ ::: tip
25+
26+ 阅读本文之前,建议你先学习一下另一种字符串匹配算法:[ Rabin Karp 字符匹配算法] ( https://labuladong.online/algo/practice-in-action/rabinkarp/ ) 。
27+
28+ :::
2529
2630KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法,效率很高,但是确实有点复杂。
2731
@@ -33,7 +37,11 @@ KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法
3337
3438读者见过的 KMP 算法应该是,一波诡异的操作处理 ` pat ` 后形成一个一维的数组 ` next ` ,然后根据这个数组经过又一波复杂操作去匹配 ` txt ` 。时间复杂度 O(N),空间复杂度 O(M)。其实它这个 ` next ` 数组就相当于 ` dp ` 数组,其中元素的含义跟 ` pat ` 的前缀和后缀有关,判定规则比较复杂,不好理解。** 本文则用一个二维的 ` dp ` 数组(但空间复杂度还是 O(M)),重新定义其中元素的含义,使得代码长度大大减少,可解释性大大提高** 。
3539
36- > note:本文的代码参考《算法4》,原代码使用的数组名称是 ` dfa ` (确定有限状态机),因为我们的公众号之前有一系列动态规划的文章,就不说这么高大上的名词了,我对书中代码进行了一点修改,并沿用 ` dp ` 数组的名称。
40+ ::: note
41+
42+ 本文的代码参考《算法4》,原代码使用的数组名称是 ` dfa ` (确定有限状态机),因为我们的公众号之前有一系列动态规划的文章,就不说这么高大上的名词了,我对书中代码进行了一点修改,并沿用 ` dp ` 数组的名称。
43+
44+ :::
3745
3846### 一、KMP 算法概述
3947
@@ -65,17 +73,17 @@ int search(String pat, String txt) {
6573
6674比如 ` txt = "aaacaaab", pat = "aaab" ` :
6775
68- ![ ] ( https://labuladong.github.io/pictures /kmp/1.gif )
76+ ![ ] ( https://labuladong.online/algo/images /kmp/1.gif )
6977
7078很明显,` pat ` 中根本没有字符 c,根本没必要回退指针 ` i ` ,暴力解法明显多做了很多不必要的操作。
7179
7280KMP 算法的不同之处在于,它会花费空间来记录一些信息,在上述情况中就会显得很聪明:
7381
74- ![ ] ( https://labuladong.github.io/pictures /kmp/2.gif )
82+ ![ ] ( https://labuladong.online/algo/images /kmp/2.gif )
7583
7684再比如类似的 ` txt = "aaaaaaab", pat = "aaab" ` ,暴力解法还会和上面那个例子一样蠢蠢地回退指针 ` i ` ,而 KMP 算法又会耍聪明:
7785
78- ![ ] ( https://labuladong.github.io/pictures /kmp/3.gif )
86+ ![ ] ( https://labuladong.online/algo/images /kmp/3.gif )
7987
8088因为 KMP 算法知道字符 b 之前的字符 a 都是匹配的,所以每次只需要比较字符 b 是否被匹配就行了。
8189
@@ -98,21 +106,25 @@ pat = "aaab"
98106
99107只不过对于 ` txt1 ` 的下面这个即将出现的未匹配情况:
100108
101- ![ ] ( https://labuladong.github.io/pictures /kmp/txt1.jpg )
109+ ![ ] ( https://labuladong.online/algo/images /kmp/txt1.jpg )
102110
103111` dp ` 数组指示 ` pat ` 这样移动:
104112
105- ![ ] ( https://labuladong.github.io/pictures/kmp/txt2.jpg )
113+ ![ ] ( https://labuladong.online/algo/images/kmp/txt2.jpg )
114+
115+ ::: note
116+
117+ 这个` j ` 不要理解为索引,它的含义更准确地说应该是** 状态** (state),所以它会出现这个奇怪的位置,后文会详述。
106118
107- > note:这个 ` j ` 不要理解为索引,它的含义更准确地说应该是 ** 状态 ** (state),所以它会出现这个奇怪的位置,后文会详述。
119+ :::
108120
109121而对于 ` txt2 ` 的下面这个即将出现的未匹配情况:
110122
111- ![ ] ( https://labuladong.github.io/pictures /kmp/txt3.jpg )
123+ ![ ] ( https://labuladong.online/algo/images /kmp/txt3.jpg )
112124
113125` dp ` 数组指示 ` pat ` 这样移动:
114126
115- ![ ] ( https://labuladong.github.io/pictures /kmp/txt4.jpg )
127+ ![ ] ( https://labuladong.online/algo/images /kmp/txt4.jpg )
116128
117129明白了 ` dp ` 数组只和 ` pat ` 有关,那么我们这样设计 KMP 算法就会比较漂亮:
118130
@@ -147,45 +159,45 @@ int pos2 = kmp.search("aaaaaaab"); //4
147159
148160为什么说 KMP 算法和状态机有关呢?是这样的,我们可以认为 ` pat ` 的匹配就是状态的转移。比如当 pat = "ABABC":
149161
150- ![ ] ( https://labuladong.github.io/pictures /kmp/state.jpg )
162+ ![ ] ( https://labuladong.online/algo/images /kmp/state.jpg )
151163
152164如上图,圆圈内的数字就是状态,状态 0 是起始状态,状态 5(` pat.length ` )是终止状态。开始匹配时 ` pat ` 处于起始状态,一旦转移到终止状态,就说明在 ` txt ` 中找到了 ` pat ` 。比如说当前处于状态 2,就说明字符 "AB" 被匹配:
153165
154- ![ ] ( https://labuladong.github.io/pictures /kmp/state2.jpg )
166+ ![ ] ( https://labuladong.online/algo/images /kmp/state2.jpg )
155167
156168另外,处于不同状态时,` pat ` 状态转移的行为也不同。比如说假设现在匹配到了状态 4,如果遇到字符 A 就应该转移到状态 3,遇到字符 C 就应该转移到状态 5,如果遇到字符 B 就应该转移到状态 0:
157169
158- ![ ] ( https://labuladong.github.io/pictures /kmp/state4.jpg )
170+ ![ ] ( https://labuladong.online/algo/images /kmp/state4.jpg )
159171
160172具体什么意思呢,我们来一个个举例看看。用变量 ` j ` 表示指向当前状态的指针,当前 ` pat ` 匹配到了状态 4:
161173
162- ![ ] ( https://labuladong.github.io/pictures /kmp/exp1.jpg )
174+ ![ ] ( https://labuladong.online/algo/images /kmp/exp1.jpg )
163175
164176如果遇到了字符 "A",根据箭头指示,转移到状态 3 是最聪明的:
165177
166- ![ ] ( https://labuladong.github.io/pictures /kmp/exp3.jpg )
178+ ![ ] ( https://labuladong.online/algo/images /kmp/exp3.jpg )
167179
168180如果遇到了字符 "B",根据箭头指示,只能转移到状态 0(一夜回到解放前):
169181
170- ![ ] ( https://labuladong.github.io/pictures /kmp/exp5.jpg )
182+ ![ ] ( https://labuladong.online/algo/images /kmp/exp5.jpg )
171183
172184如果遇到了字符 "C",根据箭头指示,应该转移到终止状态 5,这也就意味着匹配完成:
173185
174- ![ ] ( https://labuladong.github.io/pictures /kmp/exp7.jpg )
186+ ![ ] ( https://labuladong.online/algo/images /kmp/exp7.jpg )
175187
176188当然了,还可能遇到其他字符,比如 Z,但是显然应该转移到起始状态 0,因为 ` pat ` 中根本都没有字符 Z:
177189
178- ![ ] ( https://labuladong.github.io/pictures /kmp/z.jpg )
190+ ![ ] ( https://labuladong.online/algo/images /kmp/z.jpg )
179191
180192这里为了清晰起见,我们画状态图时就把其他字符转移到状态 0 的箭头省略,只画 ` pat ` 中出现的字符的状态转移:
181193
182- ![ ] ( https://labuladong.github.io/pictures /kmp/allstate.jpg )
194+ ![ ] ( https://labuladong.online/algo/images /kmp/allstate.jpg )
183195
184196KMP 算法最关键的步骤就是构造这个状态转移图。** 要确定状态转移的行为,得明确两个变量,一个是当前的匹配状态,另一个是遇到的字符** ;确定了这两个变量后,就可以知道这个情况下应该转移到哪个状态。
185197
186198下面看一下 KMP 算法根据这幅状态转移图匹配字符串 ` txt ` 的过程:
187199
188- ![ ] ( https://labuladong.github.io/pictures /kmp/kmp.gif )
200+ ![ ] ( https://labuladong.online/algo/images /kmp/kmp.gif )
189201
190202** 请记住这个 GIF 的匹配过程,这就是 KMP 算法的核心逻辑** !
191203
@@ -241,29 +253,29 @@ for 0 <= j < M: # 状态
241253
242254这个 next 状态应该怎么求呢?显然,** 如果遇到的字符 `c` 和 `pat[j]` 匹配的话** ,状态就应该向前推进一个,也就是说 `next = j + 1 ` ,我们不妨称这种情况为** 状态推进** :
243255
244- 
256+ 
245257
246258** 如果字符 `c` 和 `pat[j]` 不匹配的话** ,状态就要回退(或者原地不动),我们不妨称这种情况为** 状态重启** :
247259
248- 
260+ 
249261
250262那么,如何得知在哪个状态重启呢?解答这个问题之前,我们再定义一个名字:** 影子状态** (我编的名字),用变量 `X` 表示。** 所谓影子状态,就是和当前状态具有相同的前缀** 。比如下面这种情况:
251263
252- 
264+ 
253265
254266当前状态 `j = 4 ` ,其影子状态为 `X = 2 ` ,它们都有相同的前缀 " AB" 。因为状态 `X` 和状态 `j` 存在相同的前缀,所以当状态 `j` 准备进行状态重启的时候(遇到的字符 `c` 和 `pat[j]` 不匹配),可以通过 `X` 的状态转移图来获得** 最近的重启位置** 。
255267
256268比如说刚才的情况,如果状态 `j` 遇到一个字符 " A" ,应该转移到哪里呢?首先只有遇到 " C" 才能推进状态,遇到 " A" 显然只能进行状态重启。** 状态 `j` 会把这个字符委托给状态 `X` 处理,也就是 `dp[j][' A' ] = dp[X][' A' ]` ** :
257269
258- 
270+ 
259271
260272为什么这样可以呢?因为:既然 `j` 这边已经确定字符 " A" 无法推进状态,** 只能回退** ,而且 KMP 就是要** 尽可能少的回退** ,以免多余的计算。那么 `j` 就可以去问问和自己具有相同前缀的 `X` ,如果 `X` 遇见 " A" 可以进行「状态推进」,那就转移过去,因为这样回退最少。
261273
262- 
274+ 
263275
264276当然,如果遇到的字符是 " B" ,状态 `X` 也不能进行「状态推进」,只能回退,`j` 只要跟着 `X` 指引的方向回退就行了:
265277
266- 
278+ 
267279
268280你也许会问,这个 `X` 怎么知道遇到字符 " B" 要回退到状态 0 呢?因为 `X` 永远跟在 `j` 的身后,状态 `X` 如何转移,在之前就已经算出来了。动态规划算法不就是利用过去的结果解决现在的问题吗?
269281
@@ -358,7 +370,7 @@ for (int i = 0; i < N; i++) {
358370
359371下面来看一下状态转移图的完整构造过程,你就能理解状态 `X` 作用之精妙了:
360372
361- 
373+ 
362374
363375至此,KMP 算法的核心终于写完啦啦啦啦!看下 KMP 算法的完整代码吧:
364376
@@ -432,8 +444,8 @@ KMP 算法也就是动态规划那点事,我们的公众号文章目录有一
432444< details class =" hint-container details" >
433445< summary>< strong> 引用本文的文章< / strong>< / summary>
434446
435- - [我的刷题心得](https:// labuladong.github.io / article / fname.html ? fname = 算法心得 )
436- - [滑动窗口算法延伸:Rabin Karp 字符匹配算法](https:// labuladong.github.io / article / fname.html ? fname = rabinkarp)
447+ - [我的刷题心得:算法的本质 ](https:// labuladong.online / algo / essential - technique / algorithm - summary / )
448+ - [滑动窗口算法延伸:Rabin Karp 字符匹配算法](https:// labuladong.online / algo / practice - in - action / rabinkarp/ )
437449
438450< / details>< hr>
439451
@@ -443,9 +455,9 @@ KMP 算法也就是动态规划那点事,我们的公众号文章目录有一
443455
444456** _____________**
445457
446- ** 《labuladong 的算法小抄 》已经出版,关注公众号查看详情;后台回复「** 全家桶** 」可下载配套 PDF 和刷题全家桶** :
458+ ** 《labuladong 的算法笔记 》已经出版,关注公众号查看详情;后台回复「** 全家桶** 」可下载配套 PDF 和刷题全家桶** :
447459
448- 
460+ 
449461
450462====== 其他语言代码======
451463
0 commit comments