动态规划之KMP字符匹配算法.md 23.4 KB
Newer Older
L
labuladong 已提交
1 2
---
title: '动态规划之KMP字符匹配算法'
L
labuladong 已提交
3
tags: ['字符串']
L
labuladong 已提交
4
---
L
labuladong 已提交
5

6 7
<p align='center'>
<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>
L
labuladong 已提交
8
<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>
9 10 11 12
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
13
![](https://labuladong.github.io/pictures/souyisou1.png)
14

L
labuladong 已提交
15
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.1,[手把手刷二叉树系列课程](https://aep.xet.tech/s/3YGcq3) 上线。[第 18 期每日打卡](https://aep.xet.tech/s/2PLO1n) 开始报名。反馈或修正 chatGPT 翻译的多语言代码 [点击这里](https://github.com/labuladong/fucking-algorithm/issues/1113)。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
16 17


L
labuladong 已提交
18 19 20 21 22

读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:

| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
L
labuladong 已提交
23
| [28. Find the Index of the First Occurrence in a String](https://leetcode.com/problems/find-the-index-of-the-first-occurrence-in-a-string/) | [28. 找出字符串中第一个匹配项的下标](https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/) | 🟠
24 25 26

**-----------**

L
labuladong 已提交
27
> tip:阅读本文之前,建议你先学习一下另一种字符串匹配算法:[Rabin Karp 字符匹配算法](https://labuladong.github.io/article/fname.html?fname=rabinkarp)。
L
labuladong 已提交
28

L
labuladong 已提交
29 30 31 32 33 34 35 36 37 38
KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法,效率很高,但是确实有点复杂。

很多读者抱怨 KMP 算法无法理解,这很正常,想到大学教材上关于 KMP 算法的讲解,也不知道有多少未来的 Knuth、Morris、Pratt 被提前劝退了。有一些优秀的同学通过手推 KMP 算法的过程来辅助理解该算法,这是一种办法,不过本文要从逻辑层面帮助读者理解算法的原理。十行代码之间,KMP 灰飞烟灭。

**先在开头约定,本文用 `pat` 表示模式串,长度为 `M`,`txt` 表示文本串,长度为 `N`。KMP 算法是在 `txt` 中查找子串 `pat`,如果存在,返回这个子串的起始索引,否则返回 -1**

为什么我认为 KMP 算法就是个动态规划问题呢,等会再解释。对于动态规划,之前多次强调了要明确 `dp` 数组的含义,而且同一个问题可能有不止一种定义 `dp` 数组含义的方法,不同的定义会有不同的解法。

读者见过的 KMP 算法应该是,一波诡异的操作处理 `pat` 后形成一个一维的数组 `next`,然后根据这个数组经过又一波复杂操作去匹配 `txt`。时间复杂度 O(N),空间复杂度 O(M)。其实它这个 `next` 数组就相当于 `dp` 数组,其中元素的含义跟 `pat` 的前缀和后缀有关,判定规则比较复杂,不好理解。**本文则用一个二维的 `dp` 数组(但空间复杂度还是 O(M)),重新定义其中元素的含义,使得代码长度大大减少,可解释性大大提高**

L
labuladong 已提交
39
> note:本文的代码参考《算法4》,原代码使用的数组名称是 `dfa`(确定有限状态机),因为我们的公众号之前有一系列动态规划的文章,就不说这么高大上的名词了,我对书中代码进行了一点修改,并沿用 `dp` 数组的名称。
L
labuladong 已提交
40 41 42 43 44

### 一、KMP 算法概述

首先还是简单介绍一下 KMP 算法和暴力匹配算法的不同在哪里,难点在哪里,和动态规划有啥关系。

L
labuladong 已提交
45
力扣第 28 题「实现 strStr」就是字符串匹配问题,暴力的字符串匹配算法很容易写,看一下它的运行逻辑:
L
labuladong 已提交
46

L
labuladong 已提交
47
<!-- muliti_language -->
L
labuladong 已提交
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
```java
// 暴力匹配(伪码)
int search(String pat, String txt) {
    int M = pat.length;
    int N = txt.length;
    for (int i = 0; i <= N - M; i++) {
        int j;
        for (j = 0; j < M; j++) {
            if (pat[j] != txt[i+j])
                break;
        }
        // pat 全都匹配了
        if (j == M) return i;
    }
    // txt 中不存在 pat 子串
    return -1;
}
```

67
对于暴力算法,如果出现不匹配字符,同时回退 `txt``pat` 的指针,嵌套 for 循环,时间复杂度 `O(MN)`,空间复杂度`O(1)`。最主要的问题是,如果字符串中重复的字符比较多,该算法就显得很蠢。
L
labuladong 已提交
68

L
labuladong 已提交
69
比如 `txt = "aaacaaab", pat = "aaab"`
L
labuladong 已提交
70

L
labuladong 已提交
71
![](https://labuladong.github.io/pictures/kmp/1.gif)
L
labuladong 已提交
72 73 74 75 76

很明显,`pat` 中根本没有字符 c,根本没必要回退指针 `i`,暴力解法明显多做了很多不必要的操作。

KMP 算法的不同之处在于,它会花费空间来记录一些信息,在上述情况中就会显得很聪明:

L
labuladong 已提交
77
![](https://labuladong.github.io/pictures/kmp/2.gif)
L
labuladong 已提交
78

L
labuladong 已提交
79
再比如类似的 `txt = "aaaaaaab", pat = "aaab"`,暴力解法还会和上面那个例子一样蠢蠢地回退指针 `i`,而 KMP 算法又会耍聪明:
L
labuladong 已提交
80

L
labuladong 已提交
81
![](https://labuladong.github.io/pictures/kmp/3.gif)
L
labuladong 已提交
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103

因为 KMP 算法知道字符 b 之前的字符 a 都是匹配的,所以每次只需要比较字符 b 是否被匹配就行了。

**KMP 算法永不回退 `txt` 的指针 `i`,不走回头路(不会重复扫描 `txt`),而是借助 `dp` 数组中储存的信息把 `pat` 移到正确的位置继续匹配**,时间复杂度只需 O(N),用空间换时间,所以我认为它是一种动态规划算法。

KMP 算法的难点在于,如何计算 `dp` 数组中的信息?如何根据这些信息正确地移动 `pat` 的指针?这个就需要**确定有限状态自动机**来辅助了,别怕这种高大上的文学词汇,其实和动态规划的 `dp` 数组如出一辙,等你学会了也可以拿这个词去吓唬别人。

还有一点需要明确的是:**计算这个 `dp` 数组,只和 `pat` 串有关**。意思是说,只要给我个 `pat`,我就能通过这个模式串计算出 `dp` 数组,然后你可以给我不同的 `txt`,我都不怕,利用这个 `dp` 数组我都能在 O(N) 时间完成字符串匹配。

具体来说,比如上文举的两个例子:

```python
txt1 = "aaacaaab" 
pat = "aaab"
txt2 = "aaaaaaab" 
pat = "aaab"
```

我们的 `txt` 不同,但是 `pat` 是一样的,所以 KMP 算法使用的 `dp` 数组是同一个。

只不过对于 `txt1` 的下面这个即将出现的未匹配情况:

L
labuladong 已提交
104
![](https://labuladong.github.io/pictures/kmp/txt1.jpg)
L
labuladong 已提交
105 106 107

`dp` 数组指示 `pat` 这样移动:

L
labuladong 已提交
108
![](https://labuladong.github.io/pictures/kmp/txt2.jpg)
L
labuladong 已提交
109

L
labuladong 已提交
110
> note:这个`j` 不要理解为索引,它的含义更准确地说应该是**状态**(state),所以它会出现这个奇怪的位置,后文会详述。
L
labuladong 已提交
111 112 113

而对于 `txt2` 的下面这个即将出现的未匹配情况:

L
labuladong 已提交
114
![](https://labuladong.github.io/pictures/kmp/txt3.jpg)
L
labuladong 已提交
115 116 117

`dp` 数组指示 `pat` 这样移动:

L
labuladong 已提交
118
![](https://labuladong.github.io/pictures/kmp/txt4.jpg)
L
labuladong 已提交
119 120 121

明白了 `dp` 数组只和 `pat` 有关,那么我们这样设计 KMP 算法就会比较漂亮:

L
labuladong 已提交
122
<!-- muliti_language -->
L
labuladong 已提交
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
```java
public class KMP {
    private int[][] dp;
    private String pat;

    public KMP(String pat) {
        this.pat = pat;
        // 通过 pat 构建 dp 数组
        // 需要 O(M) 时间
    }

    public int search(String txt) {
        // 借助 dp 数组去匹配 txt
        // 需要 O(N) 时间
    }
}
```

这样,当我们需要用同一 `pat` 去匹配不同 `txt` 时,就不需要浪费时间构造 `dp` 数组了:

```java
KMP kmp = new KMP("aaab");
int pos1 = kmp.search("aaacaaab"); //4
int pos2 = kmp.search("aaaaaaab"); //4
```

### 二、状态机概述

为什么说 KMP 算法和状态机有关呢?是这样的,我们可以认为 `pat` 的匹配就是状态的转移。比如当 pat = "ABABC":

L
labuladong 已提交
153
![](https://labuladong.github.io/pictures/kmp/state.jpg)
L
labuladong 已提交
154 155 156

如上图,圆圈内的数字就是状态,状态 0 是起始状态,状态 5(`pat.length`)是终止状态。开始匹配时 `pat` 处于起始状态,一旦转移到终止状态,就说明在 `txt` 中找到了 `pat`。比如说当前处于状态 2,就说明字符 "AB" 被匹配:

L
labuladong 已提交
157
![](https://labuladong.github.io/pictures/kmp/state2.jpg)
L
labuladong 已提交
158 159 160

另外,处于不同状态时,`pat` 状态转移的行为也不同。比如说假设现在匹配到了状态 4,如果遇到字符 A 就应该转移到状态 3,遇到字符 C 就应该转移到状态 5,如果遇到字符 B 就应该转移到状态 0:

L
labuladong 已提交
161
![](https://labuladong.github.io/pictures/kmp/state4.jpg)
L
labuladong 已提交
162 163 164

具体什么意思呢,我们来一个个举例看看。用变量 `j` 表示指向当前状态的指针,当前 `pat` 匹配到了状态 4:

L
labuladong 已提交
165
![](https://labuladong.github.io/pictures/kmp/exp1.jpg)
L
labuladong 已提交
166 167 168

如果遇到了字符 "A",根据箭头指示,转移到状态 3 是最聪明的:

L
labuladong 已提交
169
![](https://labuladong.github.io/pictures/kmp/exp3.jpg)
L
labuladong 已提交
170 171 172

如果遇到了字符 "B",根据箭头指示,只能转移到状态 0(一夜回到解放前):

L
labuladong 已提交
173
![](https://labuladong.github.io/pictures/kmp/exp5.jpg)
L
labuladong 已提交
174 175 176

如果遇到了字符 "C",根据箭头指示,应该转移到终止状态 5,这也就意味着匹配完成:

L
labuladong 已提交
177
![](https://labuladong.github.io/pictures/kmp/exp7.jpg)
L
labuladong 已提交
178 179 180

当然了,还可能遇到其他字符,比如 Z,但是显然应该转移到起始状态 0,因为 `pat` 中根本都没有字符 Z:

L
labuladong 已提交
181
![](https://labuladong.github.io/pictures/kmp/z.jpg)
L
labuladong 已提交
182 183 184

这里为了清晰起见,我们画状态图时就把其他字符转移到状态 0 的箭头省略,只画 `pat` 中出现的字符的状态转移:

L
labuladong 已提交
185
![](https://labuladong.github.io/pictures/kmp/allstate.jpg)
L
labuladong 已提交
186 187 188 189 190

KMP 算法最关键的步骤就是构造这个状态转移图。**要确定状态转移的行为,得明确两个变量,一个是当前的匹配状态,另一个是遇到的字符**;确定了这两个变量后,就可以知道这个情况下应该转移到哪个状态。

下面看一下 KMP 算法根据这幅状态转移图匹配字符串 `txt` 的过程:

L
labuladong 已提交
191
![](https://labuladong.github.io/pictures/kmp/kmp.gif)
L
labuladong 已提交
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213

**请记住这个 GIF 的匹配过程,这就是 KMP 算法的核心逻辑**

为了描述状态转移图,我们定义一个二维 dp 数组,它的含义如下:

```python
dp[j][c] = next
0 <= j < M代表当前的状态
0 <= c < 256代表遇到的字符ASCII 
0 <= next <= M代表下一个状态

dp[4]['A'] = 3 表示
当前是状态 4如果遇到字符 A
pat 应该转移到状态 3

dp[1]['B'] = 2 表示
当前是状态 1如果遇到字符 B
pat 应该转移到状态 2
```

根据我们这个 dp 数组的定义和刚才状态转移的过程,我们可以先写出 KMP 算法的 search 函数代码:

L
labuladong 已提交
214
<!-- muliti_language -->
L
labuladong 已提交
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
```java
public int search(String txt) {
    int M = pat.length();
    int N = txt.length();
    // pat 的初始态为 0
    int j = 0;
    for (int i = 0; i < N; i++) {
        // 当前是状态 j,遇到字符 txt[i],
        // pat 应该转移到哪个状态?
        j = dp[j][txt.charAt(i)];
        // 如果达到终止态,返回匹配开头的索引
        if (j == M) return i - M + 1;
    }
    // 没到达终止态,匹配失败
    return -1;
}
```

到这里,应该还是很好理解的吧,`dp` 数组就是我们刚才画的那幅状态转移图,如果不清楚的话回去看下 GIF 的算法演进过程。下面讲解:如何通过 `pat` 构建这个 `dp` 数组?

### 三、构建状态转移图

回想刚才说的:**要确定状态转移的行为,必须明确两个变量,一个是当前的匹配状态,另一个是遇到的字符**,而且我们已经根据这个逻辑确定了 `dp` 数组的含义,那么构造 `dp` 数组的框架就是这样:

```python
for 0 <= j < M: # 状态
    for 0 <= c < 256: # 字符
        dp[j][c] = next
```

这个 next 状态应该怎么求呢?显然,**如果遇到的字符 `c` 和 `pat[j]` 匹配的话**,状态就应该向前推进一个,也就是说 `next = j + 1`,我们不妨称这种情况为**状态推进**

L
labuladong 已提交
247
![](https://labuladong.github.io/pictures/kmp/forward.jpg)
L
labuladong 已提交
248 249 250

**如果字符 `c` 和 `pat[j]` 不匹配的话**,状态就要回退(或者原地不动),我们不妨称这种情况为**状态重启**

L
labuladong 已提交
251
![](https://labuladong.github.io/pictures/kmp/back.jpg)
L
labuladong 已提交
252 253 254

那么,如何得知在哪个状态重启呢?解答这个问题之前,我们再定义一个名字:**影子状态**(我编的名字),用变量 `X` 表示。**所谓影子状态,就是和当前状态具有相同的前缀**。比如下面这种情况:

L
labuladong 已提交
255
![](https://labuladong.github.io/pictures/kmp/shadow.jpg)
L
labuladong 已提交
256 257 258 259 260

当前状态 `j = 4`,其影子状态为 `X = 2`,它们都有相同的前缀 "AB"。因为状态 `X` 和状态 `j` 存在相同的前缀,所以当状态 `j` 准备进行状态重启的时候(遇到的字符 `c``pat[j]` 不匹配),可以通过 `X` 的状态转移图来获得**最近的重启位置**

比如说刚才的情况,如果状态 `j` 遇到一个字符 "A",应该转移到哪里呢?首先只有遇到 "C" 才能推进状态,遇到 "A" 显然只能进行状态重启。**状态 `j` 会把这个字符委托给状态 `X` 处理,也就是 `dp[j]['A'] = dp[X]['A']`**

L
labuladong 已提交
261
![](https://labuladong.github.io/pictures/kmp/shadow1.jpg)
L
labuladong 已提交
262 263 264

为什么这样可以呢?因为:既然 `j` 这边已经确定字符 "A" 无法推进状态,**只能回退**,而且 KMP 就是要**尽可能少的回退**,以免多余的计算。那么 `j` 就可以去问问和自己具有相同前缀的 `X`,如果 `X` 遇见 "A" 可以进行「状态推进」,那就转移过去,因为这样回退最少。

L
labuladong 已提交
265
![](https://labuladong.github.io/pictures/kmp/A.gif)
L
labuladong 已提交
266 267 268

当然,如果遇到的字符是 "B",状态 `X` 也不能进行「状态推进」,只能回退,`j` 只要跟着 `X` 指引的方向回退就行了:

L
labuladong 已提交
269
![](https://labuladong.github.io/pictures/kmp/shadow2.jpg)
L
labuladong 已提交
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291

你也许会问,这个 `X` 怎么知道遇到字符 "B" 要回退到状态 0 呢?因为 `X` 永远跟在 `j` 的身后,状态 `X` 如何转移,在之前就已经算出来了。动态规划算法不就是利用过去的结果解决现在的问题吗?

这样,我们就细化一下刚才的框架代码:

```python
int X # 影子状态
for 0 <= j < M:
    for 0 <= c < 256:
        if c == pat[j]:
            # 状态推进
            dp[j][c] = j + 1
        else: 
            # 状态重启
            # 委托 X 计算重启位置
            dp[j][c] = dp[X][c] 
```

### 四、代码实现

如果之前的内容你都能理解,恭喜你,现在就剩下一个问题:影子状态 `X` 是如何得到的呢?下面先直接看完整代码吧。

L
labuladong 已提交
292
<!-- muliti_language -->
L
labuladong 已提交
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
```java
public class KMP {
    private int[][] dp;
    private String pat;

    public KMP(String pat) {
        this.pat = pat;
        int M = pat.length();
        // dp[状态][字符] = 下个状态
        dp = new int[M][256];
        // base case
        dp[0][pat.charAt(0)] = 1;
        // 影子状态 X 初始为 0
        int X = 0;
        // 当前状态 j 从 1 开始
        for (int j = 1; j < M; j++) {
            for (int c = 0; c < 256; c++) {
                if (pat.charAt(j) == c) 
                    dp[j][c] = j + 1;
                else 
                    dp[j][c] = dp[X][c];
            }
            // 更新影子状态
            X = dp[X][pat.charAt(j)];
        }
    }

    public int search(String txt) {...}
}
```

先解释一下这一行代码:

```java
// base case
dp[0][pat.charAt(0)] = 1;
```

这行代码是 base case,只有遇到 pat[0] 这个字符才能使状态从 0 转移到 1,遇到其它字符的话还是停留在状态 0(Java 默认初始化数组全为 0)。

影子状态 `X` 是先初始化为 0,然后随着 `j` 的前进而不断更新的。下面看看到底应该**如何更新影子状态 `X`**

```java
int X = 0;
for (int j = 1; j < M; j++) {
    ...
    // 更新影子状态
    // 当前是状态 X,遇到字符 pat[j],
    // pat 应该转移到哪个状态?
    X = dp[X][pat.charAt(j)];
}
```

更新 `X` 其实和 `search` 函数中更新状态 `j` 的过程是非常相似的:

```java
int j = 0;
for (int i = 0; i < N; i++) {
    // 当前是状态 j,遇到字符 txt[i],
    // pat 应该转移到哪个状态?
    j = dp[j][txt.charAt(i)];
    ...
}
```

**其中的原理非常微妙**,注意代码中 for 循环的变量初始值,可以这样理解:后者是在 `txt` 中匹配 `pat`,前者是在 `pat` 中匹配 `pat[1..end]`,状态 `X` 总是落后状态 `j` 一个状态,与 `j` 具有最长的相同前缀。所以我把 `X` 比喻为影子状态,似乎也有一点贴切。

另外,构建 dp 数组是根据 base case `dp[0][..]` 向后推演。这就是我认为 KMP 算法就是一种动态规划算法的原因。

下面来看一下状态转移图的完整构造过程,你就能理解状态 `X` 作用之精妙了:

L
labuladong 已提交
364
![](https://labuladong.github.io/pictures/kmp/dfa.gif)
L
labuladong 已提交
365 366 367

至此,KMP 算法的核心终于写完啦啦啦啦!看下 KMP 算法的完整代码吧:

L
labuladong 已提交
368
<!-- muliti_language -->
L
labuladong 已提交
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
```java
public class KMP {
    private int[][] dp;
    private String pat;

    public KMP(String pat) {
        this.pat = pat;
        int M = pat.length();
        // dp[状态][字符] = 下个状态
        dp = new int[M][256];
        // base case
        dp[0][pat.charAt(0)] = 1;
        // 影子状态 X 初始为 0
        int X = 0;
        // 构建状态转移图(稍改的更紧凑了)
        for (int j = 1; j < M; j++) {
            for (int c = 0; c < 256; c++)
                dp[j][c] = dp[X][c];
            dp[j][pat.charAt(j)] = j + 1;
            // 更新影子状态
            X = dp[X][pat.charAt(j)];
        }
    }

    public int search(String txt) {
        int M = pat.length();
        int N = txt.length();
        // pat 的初始态为 0
        int j = 0;
        for (int i = 0; i < N; i++) {
            // 计算 pat 的下一个状态
            j = dp[j][txt.charAt(i)];
            // 到达终止态,返回结果
            if (j == M) return i - M + 1;
        }
        // 没到达终止态,匹配失败
        return -1;
    }
}
```

经过之前的详细举例讲解,你应该可以理解这段代码的含义了,当然你也可以把 KMP 算法写成一个函数。核心代码也就是两个函数中 for 循环的部分,数一下有超过十行吗?

### 五、最后总结

传统的 KMP 算法是使用一个一维数组 `next` 记录前缀信息,而本文是使用一个二维数组 `dp` 以状态转移的角度解决字符匹配问题,但是空间复杂度仍然是 O(256M) = O(M)。

`pat` 匹配 `txt` 的过程中,只要明确了「当前处在哪个状态」和「遇到的字符是什么」这两个问题,就可以确定应该转移到哪个状态(推进或回退)。

对于一个模式串 `pat`,其总共就有 M 个状态,对于 ASCII 字符,总共不会超过 256 种。所以我们就构造一个数组 `dp[M][256]` 来包含所有情况,并且明确 `dp` 数组的含义:

`dp[j][c] = next` 表示,当前是状态 `j`,遇到了字符 `c`,应该转移到状态 `next`

明确了其含义,就可以很容易写出 search 函数的代码。

对于如何构建这个 `dp` 数组,需要一个辅助状态 `X`,它永远比当前状态 `j` 落后一个状态,拥有和 `j` 最长的相同前缀,我们给它起了个名字叫「影子状态」。

在构建当前状态 `j` 的转移方向时,只有字符 `pat[j]` 才能使状态推进(`dp[j][pat[j]] = j+1`);而对于其他字符只能进行状态回退,应该去请教影子状态 `X` 应该回退到哪里(`dp[j][other] = dp[X][other]`,其中 `other` 是除了 `pat[j]` 之外所有字符)。

对于影子状态 `X`,我们把它初始化为 0,并且随着 `j` 的前进进行更新,更新的方式和 search 过程更新 `j` 的过程非常相似(`X = dp[X][pat[j]]`)。

KMP 算法也就是动态规划那点事,我们的公众号文章目录有一系列专门讲动态规划的,而且都是按照一套框架来的,无非就是描述问题逻辑,明确 `dp` 数组含义,定义 base case 这点破事。希望这篇文章能让大家对动态规划有更深的理解。


L
labuladong 已提交
433

L
labuladong 已提交
434 435 436 437 438 439 440 441 442 443 444 445 446
<hr>
<details>
<summary><strong>引用本文的文章</strong></summary>

 - [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)
 - [滑动窗口算法延伸:Rabin Karp 字符匹配算法](https://labuladong.github.io/article/fname.html?fname=rabinkarp)

</details><hr>





447 448
**_____________**

L
labuladong 已提交
449
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
450

L
labuladong 已提交
451
![](https://labuladong.github.io/pictures/souyisou2.png)
L
labuladong 已提交
452 453


M
MoguCloud 已提交
454
======其他语言代码======
B
brucecat 已提交
455 456 457 458 459

[28.实现 strStr()](https://leetcode-cn.com/problems/implement-strstr)

### python

M
MoguCloud 已提交
460
[MoguCloud](https://github.com/MoguCloud) 提供 实现 strStr() 的 Python 完整代码:
B
brucecat 已提交
461 462

```python
M
MoguCloud 已提交
463
class Solution:
B
brucecat 已提交
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
  def strStr(self, haystack: str, needle: str) -> int:
    # 边界条件判断
    if not needle:
      return 0
    pat = needle
    txt = haystack

    M = len(pat)
    # dp[状态][字符] = 下个状态
    dp = [[0 for _ in range(256)] for _ in pat]
    # base case
    dp[0][ord(pat[0])] = 1
    # 影子状态 X 初始化为 0
    X = 0
    for j in range(1, M):
      for c in range(256):
        dp[j][c] = dp[X][c]
        dp[j][ord(pat[j])] = j + 1
        # 更新影子状态
        X = dp[X][ord(pat[j])]

M
MoguCloud 已提交
485 486 487 488
        N = len(txt)
        # pat 初始状态为 0 
        j = 0
        for i in range(N):
B
brucecat 已提交
489 490 491 492 493 494 495
          # 计算 pat 的下一个状态
          j = dp[j][ord(txt[i])]
          # 到达终止态,返回结果
          if j == M:
            return i - M + 1
          # 没到达终止态,匹配失败
          return -1
M
MoguCloud 已提交
496
```
B
brucecat 已提交
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578



### javascript

```js
class KMP {
  constructor(pat) {
    this.pat = pat;
    let m = pat.length;

    // dp[状态][字符] = 下个状态  初始化一个m*256的整数矩阵
    this.dp = new Array(m);
    for (let i = 0; i < m; i++) {
      this.dp[i] = new Array(256);
      this.dp[i].fill(0, 0, 256);
    }

    // base case
    this.dp[0][this.pat[0].charCodeAt()] = 1;

    // 影子状态X 初始为0
    let x = 0;

    // 构建状态转移图
    for (let j = 1; j < m; j++) {
      for (let c = 0; c < 256; c++) {
        this.dp[j][c] = this.dp[x][c];
      }

      // dp[][对应的ASCII码]
      this.dp[j][this.pat[j].charCodeAt()] = j + 1;

      // 更新影子状态
      x = this.dp[x][this.pat[j].charCodeAt()]
    }
  }

  search(txt) {

    let m = this.pat.length;
    let n = txt.length;

    // pat的初始态为0
    let j = 0;
    for (let i = 0; i < n; i++) {
      // 计算pat的下一个状态
      j = this.dp[j][txt[i].charCodeAt()];

      // 到达终止态 返回结果
      if (j === m) return i - m + 1;
    }

    // 没到终止态 匹配失败
    return -1;
  }

}

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function(haystack, needle) { 
  if(haystack === ""){
    if(needle !== ""){
      return -1;
    }
    return 0;
  }

  if(needle === ""){
    return 0;
  }
  let kmp = new KMP(needle);
  return kmp.search(haystack)
};
```