7
7
<a href =" https://space.bilibili.com/14089380 " ><img src =" https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili " ></a >
8
8
</p >
9
9
10
- ![ ] ( https://labuladong.github.io/pictures /souyisou1.png )
10
+ ![ ] ( https://labuladong.online/algo/images /souyisou1.png )
11
11
12
12
** 通知:[ 新版网站会员] ( https://labuladong.online/algo/intro/site-vip/ ) 限时优惠;算法可视化编辑器上线,[ 点击体验] ( https://labuladong.online/algo/intro/visualize/ ) !另外,建议你在我的 [ 网站] ( https://labuladong.online/algo/ ) 学习文章,体验更好。**
13
13
@@ -73,17 +73,17 @@ int search(String pat, String txt) {
73
73
74
74
比如 ` txt = "aaacaaab", pat = "aaab" ` :
75
75
76
- ![ ] ( https://labuladong.github.io/pictures /kmp/1.gif )
76
+ ![ ] ( https://labuladong.online/algo/images /kmp/1.gif )
77
77
78
78
很明显,` pat ` 中根本没有字符 c,根本没必要回退指针 ` i ` ,暴力解法明显多做了很多不必要的操作。
79
79
80
80
KMP 算法的不同之处在于,它会花费空间来记录一些信息,在上述情况中就会显得很聪明:
81
81
82
- ![ ] ( https://labuladong.github.io/pictures /kmp/2.gif )
82
+ ![ ] ( https://labuladong.online/algo/images /kmp/2.gif )
83
83
84
84
再比如类似的 ` txt = "aaaaaaab", pat = "aaab" ` ,暴力解法还会和上面那个例子一样蠢蠢地回退指针 ` i ` ,而 KMP 算法又会耍聪明:
85
85
86
- ![ ] ( https://labuladong.github.io/pictures /kmp/3.gif )
86
+ ![ ] ( https://labuladong.online/algo/images /kmp/3.gif )
87
87
88
88
因为 KMP 算法知道字符 b 之前的字符 a 都是匹配的,所以每次只需要比较字符 b 是否被匹配就行了。
89
89
@@ -106,11 +106,11 @@ pat = "aaab"
106
106
107
107
只不过对于 ` txt1 ` 的下面这个即将出现的未匹配情况:
108
108
109
- ![ ] ( https://labuladong.github.io/pictures /kmp/txt1.jpg )
109
+ ![ ] ( https://labuladong.online/algo/images /kmp/txt1.jpg )
110
110
111
111
` dp ` 数组指示 ` pat ` 这样移动:
112
112
113
- ![ ] ( https://labuladong.github.io/pictures /kmp/txt2.jpg )
113
+ ![ ] ( https://labuladong.online/algo/images /kmp/txt2.jpg )
114
114
115
115
::: note
116
116
@@ -120,11 +120,11 @@ pat = "aaab"
120
120
121
121
而对于 ` txt2 ` 的下面这个即将出现的未匹配情况:
122
122
123
- ![ ] ( https://labuladong.github.io/pictures /kmp/txt3.jpg )
123
+ ![ ] ( https://labuladong.online/algo/images /kmp/txt3.jpg )
124
124
125
125
` dp ` 数组指示 ` pat ` 这样移动:
126
126
127
- ![ ] ( https://labuladong.github.io/pictures /kmp/txt4.jpg )
127
+ ![ ] ( https://labuladong.online/algo/images /kmp/txt4.jpg )
128
128
129
129
明白了 ` dp ` 数组只和 ` pat ` 有关,那么我们这样设计 KMP 算法就会比较漂亮:
130
130
@@ -159,45 +159,45 @@ int pos2 = kmp.search("aaaaaaab"); //4
159
159
160
160
为什么说 KMP 算法和状态机有关呢?是这样的,我们可以认为 ` pat ` 的匹配就是状态的转移。比如当 pat = "ABABC":
161
161
162
- ![ ] ( https://labuladong.github.io/pictures /kmp/state.jpg )
162
+ ![ ] ( https://labuladong.online/algo/images /kmp/state.jpg )
163
163
164
164
如上图,圆圈内的数字就是状态,状态 0 是起始状态,状态 5(` pat.length ` )是终止状态。开始匹配时 ` pat ` 处于起始状态,一旦转移到终止状态,就说明在 ` txt ` 中找到了 ` pat ` 。比如说当前处于状态 2,就说明字符 "AB" 被匹配:
165
165
166
- ![ ] ( https://labuladong.github.io/pictures /kmp/state2.jpg )
166
+ ![ ] ( https://labuladong.online/algo/images /kmp/state2.jpg )
167
167
168
168
另外,处于不同状态时,` pat ` 状态转移的行为也不同。比如说假设现在匹配到了状态 4,如果遇到字符 A 就应该转移到状态 3,遇到字符 C 就应该转移到状态 5,如果遇到字符 B 就应该转移到状态 0:
169
169
170
- ![ ] ( https://labuladong.github.io/pictures /kmp/state4.jpg )
170
+ ![ ] ( https://labuladong.online/algo/images /kmp/state4.jpg )
171
171
172
172
具体什么意思呢,我们来一个个举例看看。用变量 ` j ` 表示指向当前状态的指针,当前 ` pat ` 匹配到了状态 4:
173
173
174
- ![ ] ( https://labuladong.github.io/pictures /kmp/exp1.jpg )
174
+ ![ ] ( https://labuladong.online/algo/images /kmp/exp1.jpg )
175
175
176
176
如果遇到了字符 "A",根据箭头指示,转移到状态 3 是最聪明的:
177
177
178
- ![ ] ( https://labuladong.github.io/pictures /kmp/exp3.jpg )
178
+ ![ ] ( https://labuladong.online/algo/images /kmp/exp3.jpg )
179
179
180
180
如果遇到了字符 "B",根据箭头指示,只能转移到状态 0(一夜回到解放前):
181
181
182
- ![ ] ( https://labuladong.github.io/pictures /kmp/exp5.jpg )
182
+ ![ ] ( https://labuladong.online/algo/images /kmp/exp5.jpg )
183
183
184
184
如果遇到了字符 "C",根据箭头指示,应该转移到终止状态 5,这也就意味着匹配完成:
185
185
186
- ![ ] ( https://labuladong.github.io/pictures /kmp/exp7.jpg )
186
+ ![ ] ( https://labuladong.online/algo/images /kmp/exp7.jpg )
187
187
188
188
当然了,还可能遇到其他字符,比如 Z,但是显然应该转移到起始状态 0,因为 ` pat ` 中根本都没有字符 Z:
189
189
190
- ![ ] ( https://labuladong.github.io/pictures /kmp/z.jpg )
190
+ ![ ] ( https://labuladong.online/algo/images /kmp/z.jpg )
191
191
192
192
这里为了清晰起见,我们画状态图时就把其他字符转移到状态 0 的箭头省略,只画 ` pat ` 中出现的字符的状态转移:
193
193
194
- ![ ] ( https://labuladong.github.io/pictures /kmp/allstate.jpg )
194
+ ![ ] ( https://labuladong.online/algo/images /kmp/allstate.jpg )
195
195
196
196
KMP 算法最关键的步骤就是构造这个状态转移图。** 要确定状态转移的行为,得明确两个变量,一个是当前的匹配状态,另一个是遇到的字符** ;确定了这两个变量后,就可以知道这个情况下应该转移到哪个状态。
197
197
198
198
下面看一下 KMP 算法根据这幅状态转移图匹配字符串 ` txt ` 的过程:
199
199
200
- ![ ] ( https://labuladong.github.io/pictures /kmp/kmp.gif )
200
+ ![ ] ( https://labuladong.online/algo/images /kmp/kmp.gif )
201
201
202
202
** 请记住这个 GIF 的匹配过程,这就是 KMP 算法的核心逻辑** !
203
203
@@ -253,29 +253,29 @@ for 0 <= j < M: # 状态
253
253
254
254
这个 next 状态应该怎么求呢?显然,** 如果遇到的字符 `c` 和 `pat[j]` 匹配的话** ,状态就应该向前推进一个,也就是说 `next = j + 1 ` ,我们不妨称这种情况为** 状态推进** :
255
255
256
- 
256
+ 
257
257
258
258
** 如果字符 `c` 和 `pat[j]` 不匹配的话** ,状态就要回退(或者原地不动),我们不妨称这种情况为** 状态重启** :
259
259
260
- 
260
+ 
261
261
262
262
那么,如何得知在哪个状态重启呢?解答这个问题之前,我们再定义一个名字:** 影子状态** (我编的名字),用变量 `X` 表示。** 所谓影子状态,就是和当前状态具有相同的前缀** 。比如下面这种情况:
263
263
264
- 
264
+ 
265
265
266
266
当前状态 `j = 4 ` ,其影子状态为 `X = 2 ` ,它们都有相同的前缀 " AB" 。因为状态 `X` 和状态 `j` 存在相同的前缀,所以当状态 `j` 准备进行状态重启的时候(遇到的字符 `c` 和 `pat[j]` 不匹配),可以通过 `X` 的状态转移图来获得** 最近的重启位置** 。
267
267
268
268
比如说刚才的情况,如果状态 `j` 遇到一个字符 " A" ,应该转移到哪里呢?首先只有遇到 " C" 才能推进状态,遇到 " A" 显然只能进行状态重启。** 状态 `j` 会把这个字符委托给状态 `X` 处理,也就是 `dp[j][' A' ] = dp[X][' A' ]` ** :
269
269
270
- 
270
+ 
271
271
272
272
为什么这样可以呢?因为:既然 `j` 这边已经确定字符 " A" 无法推进状态,** 只能回退** ,而且 KMP 就是要** 尽可能少的回退** ,以免多余的计算。那么 `j` 就可以去问问和自己具有相同前缀的 `X` ,如果 `X` 遇见 " A" 可以进行「状态推进」,那就转移过去,因为这样回退最少。
273
273
274
- 
274
+ 
275
275
276
276
当然,如果遇到的字符是 " B" ,状态 `X` 也不能进行「状态推进」,只能回退,`j` 只要跟着 `X` 指引的方向回退就行了:
277
277
278
- 
278
+ 
279
279
280
280
你也许会问,这个 `X` 怎么知道遇到字符 " B" 要回退到状态 0 呢?因为 `X` 永远跟在 `j` 的身后,状态 `X` 如何转移,在之前就已经算出来了。动态规划算法不就是利用过去的结果解决现在的问题吗?
281
281
@@ -370,7 +370,7 @@ for (int i = 0; i < N; i++) {
370
370
371
371
下面来看一下状态转移图的完整构造过程,你就能理解状态 `X` 作用之精妙了:
372
372
373
- 
373
+ 
374
374
375
375
至此,KMP 算法的核心终于写完啦啦啦啦!看下 KMP 算法的完整代码吧:
376
376
@@ -457,7 +457,7 @@ KMP 算法也就是动态规划那点事,我们的公众号文章目录有一
457
457
458
458
** 《labuladong 的算法笔记》已经出版,关注公众号查看详情;后台回复「** 全家桶** 」可下载配套 PDF 和刷题全家桶** :
459
459
460
- 
460
+ 
461
461
462
462
====== 其他语言代码======
463
463
0 commit comments