通过看labuladong算法讲解,自己总结了笔记:
概述
⽤ pat 表⽰模式串,⻓度为 M , txt 表⽰⽂本串, ⻓度为 N 。KMP 算法是在 txt 中查找⼦串 pat ,如果存在,返回这个 ⼦串的起始索引,否则返回 -1。
先看暴力解法:
会出现 txt 指针回退现象,KMP 算法花费空间减少时间消耗。KMP 算法永不回退 txt 的指针 i ,不⾛回头路(不会重复扫描 txt ),⽽是借助 dp 数组中储存的信息把 pat 移到正确的位置继续匹 配,时间复杂度只需 O(N),⽤空间换时间,所以我认为它是⼀种动态规划。
注:有⼀点需要明确的是:计算这个 dp 数组,只和 pat 串有关。意思是 说,只要给我个 pat ,我就能通过这个模式串计算出 dp 数组,然后你可 以给我不同的 txt ,我都不怕,利⽤这个 dp 数组我都能在 O(N) 时间完 成字符串匹配。
状态转移:
回想刚才说的:要确定状态转移的⾏为,必须明确两个变量,⼀个是当前的 匹配状态,另⼀个是遇到的字符,⽽且我们已经根据这个逻辑确定了 dp 数组的含义,那么构造 dp 数组的框架就是这样:
for 0 <= j < M: //状态
for 0 <= c < 256: //字符
dp[j][c] = next
这个 next 状态应该怎么求呢?显然,如果遇到的字符 c 和 pat[j] 匹配 的话,状态就应该向前推进⼀个,也就是说 next = j + 1 ,我们不妨称这 种情况为状态推进:
如果字符 c 和 pat[j] 不匹配的话,状态就要回退(或者原地不动),我 们不妨称这种情况为状态重启:
那么,如何得知在哪个状态重启呢?解答这个问题之前,我们再定义⼀个名 字:影⼦状态(我编的名字),⽤变量 X 表⽰。所谓影⼦状态,就是和当 前状态具有相同的前缀。⽐如下⾯这种情况:
当前状态 j = 4 ,其影⼦状态为 X = 2 ,它们都有相同的前缀 “AB”。因 为状态 X 和状态 j 存在相同的前缀,所以当状态 j 准备进⾏状态重启 的时候(遇到的字符 c 和 pat[j] 不匹配),可以通过 X 的状态转移图 来获得最近的重启位置。
状态 j 会把这个字符委托给状态 X 处理,也就是 dp[j][‘A’] = dp[X][‘A’] :
注意影子状态
影⼦状态 X 是先初始化为 0,然后随着 j 的前进⽽不断更新的。下⾯看 看到底应该如何更新影⼦状态 X :
int X = 0;
for (int j = 1; j < M; j++) { // 更新影⼦状态
// 当前是状态 X,遇到字符 pat[j],
// pat 应该转移到哪个状态?
X = dp[X][pat.charAt(j)];
}
更新 X 其实和 search 函数中更新状态 j 的过程是⾮常相似的:
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 ⽐喻为影⼦状态,似乎也有⼀点贴切。
solution
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;
}
}