KMP算法
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
文章正式开始之前
做如下说明
- PAT: 模式串,如
ABAACA
也就是想要查找的结果。 - CharAt: 这里
ABC
是指当前用来匹配Pat
的字符,比如这里有一串文本ABABABC
。就是在ABC
这三个字符构成的文本串里面寻找内容,取3个字母是为了精简说明过程。 - 状态(M): 当前匹配的状态。
- 前一个状态(X): 记录如果失败了应该 保留 到哪些步骤之前。
- dp: 构建的状态转移数组,即表格内容。
解释
整体的流程图
取自《算法(第4版)》 的
图5.3.6
构建一个状态表,当在某个状态时,遇到了某个字符下一步应该转移到什么状态
在 j=0
这个位置成功匹配到A
,说明可以前往(Next)状态1
。
CharAt | A✔️ | Next | ||||
---|---|---|---|---|---|---|
Pat | A✔️ | B | A | B | A | C |
J | 0 | 1 | 2 | 3 | 4 | 5 |
在 j=1
这个位置成功匹配到B
,说明可以前往(Next)状态2
。
CharAt | A✔️ | B✔️ | Next | |||
---|---|---|---|---|---|---|
Pat | A✔️ | B✔️ | A | B | A | C |
J | 0 | 1 | 2 | 3 | 4 | 5 |
那么在 j=1
这个位置与A
不匹配(B
才能成功),说明可以前往状态1
。
CharAt | A✔️ | A❌ | ||||
---|---|---|---|---|---|---|
Pat | A✔️ | B❌ | A | B | A | C |
J | 0 | 1 | 2 | 3 | 4 | 5 |
此时保留Pat
的第一位A
可以与CharAt
的第二位A
匹配,继续匹配下一个位置的字符串不需要回退在CharAt
上的文本指针。
CharAt | A | A✔️ | Next | ||||
---|---|---|---|---|---|---|---|
Pat | A✔️ | B | A | B | A | C | |
J | 0 | 1 | 2 | 3 | 4 | 5 |
如何构建这个转移过程
我们将以 ABAACA来构建这个过程。
正确转移到最后一个状态,即已经匹配成功
以ABAACA
构建一个最简单的状态转移图片,它的意思是:
在状态0
匹配到字符A
转移到状态1
,在状态1
匹配到字符B
转移到状态2
,
直到在状态4
匹配到字符C
转移到状态5
则获取结果成功。这是一条目前最容易定义的转移方式。
那么为什么这么定义呢?
- 可以看到如想要到达
状态2
,必须已经满足状态1
又成功匹配了B
字符。 - 那么当想要到达
状态6
,必须已经满足状态5
又成功匹配了A
字符,也就是当前已经成功匹配了Pat(ABAACA)
。 - 后面的每一层状态都是由之前状态决定,所以可以从前往后计算下一步应该转移成什么状态,符合 动态规划 的过程。
转移过程的代码
构建转移表的过程
for 0 <= j < M: // 状态由模式串决定
for 0 <= c < 256: // 也可以是更大的包括中文等
dp[j][c] = next //下一步应该前往什么状态
识别过程
int M = 0//当前初始状态
int P = pat.length();//模式串的长度,也就是总共具有多少状态,[0...P]一共是P+1个状态
int T = txt.length()//计算文本的长度
for (int i = 0; i < T; i++) {
M = dp[M][txt.charAt(i)];//当前状态在遇到某个字符应该转移到哪里
if (M == P) {
//当到P这个状态的时候已经满足条件,返回成功的位置
return i - M + 1;
}
}
匹配成功转移表
- 在
状态0
匹配到第一个字符A
,此时模式串与字符匹配表示可以前往状态1
- 在
状态1
匹配到第二个字符B
,此时模式串与字符匹配表示可以前往状态2
- 在
状态2
匹配到第三个字符A
,此时模式串与字符匹配表示可以前往状态3
- 在
状态3
匹配到第四个字符A
,此时模式串与字符匹配表示可以前往状态4
- 在
状态4