基本思想
- 规定:匹配字符串 - 模式串(pat), 匹配文本 - 文本(txt)
- 基本思想:当出现不匹配时,就能知晓一部分文本的内容(因为在匹配失败之前它们已经和模式相匹配),根据这些已经知晓的内容决定 在出现不匹配时,模式应在处于哪个位置和文本的下一个字符比较 - 即找到已知晓内容和模式的最长公共前缀(利用模式去匹配已知晓的内容)
要点
看《算法》的时候,有点部分真的太简洁了,看的晦涩难懂( 是我太菜了~ /(ㄒoㄒ)/~~) ,关注一下几个要点,要结合书中内容看可能会有新的理解
- 指向文本的指针 i 永远不会回退,不会重复扫描文本,只有模式的指针 j 会进行回退。
- 有限状态机(DFA)只和 模式 有关,所以只要计算出了一个 模式的有限状态机(DFA),就可以匹配不同的文本。
- DFA[txt.charAt(i)][j] = next, 表示 当前状态 j 时,遇到 文本字符 txt.charAt(i) (即 txt.charAt(i) 和 pat.charAt(j)进行比较)后的下一个状态是 next ( 即模式的指针 j 需要回退/前进到 next 处 和 文本字符 txt.charAt(i + 1)进行比较) 。
- 在一个状态中,要确定状态会进行何种转移,需要知道 当前状态 和 遇到的字符。
- 构造DFA:匹配成功时,即txt.charAt(i) == pat.charAt(j),DFA[txt.charAt(i)][j] = j +1; 匹配失败时,模式指针 j 的回退也不是盲目回退, 它会根据 部分已经匹配成功的字符串与模式进行匹配后所处的状态(即书上所说的 X 重启状态) 以及 当前匹配失败的输入 来决定回退到哪个位置
知道 状态 j 遇到了 字符D 发生了不匹配,意味着pat的前 j 个字符串 (0.. j-1)和文本的 (i - j, i - 1)是相匹配的, 就像上述所示,但是我们不用理会 txt.charAt(i-j),因为 i - j处已经不可能出现匹配,所以 部分已经匹配成功的字符串 就为 B A B A (pat[1… j - 1])。
现我们考虑 B A B A 和 模式 进行匹配会到达什么状态(所到达的状态也是书中所提到的 重启状态 X),这个过程我们也可以看成是找 部分已经匹配成功的字符串 和 模式 的 最长公共前缀 的过程。 - 困扰我很久的一个 X 如何进行求得,可以将其看成一个 X[] 数组,记录了模式与 部分匹配成功的字符串(pat[1…j -1])所达到的所有状态(书本P765,图5.3.8很好的表达了此点)。它们之间关系是一个递推关系,X[i+1]为X[j]状态 遇到 pat.charAt(j)时所到达的状态,即 X[j + 1] = DFA[pat.charAt(j)][x[j]],X[0]初始化状态为0。 这也便是书中代码中的 X = DFA[pat.charAt(j)][X]。理解了上述6,7之后,就可以写出构造DFA的过程(当然,我对于上述6,7的说明都是基于你已经看过了《算法》中字符串查找部分哦~)
int[][] DFA;
public void generateDFA(String pat){
int M = pat.length();
int R = 256; // ASCII字符不会操过256种
DFA = new int[R][M];
// 初始化状态0, 在状态0只有遇到了pat.charAt(0)才会向前推进, 遇到其他为0(java默认初始化数组为0)
DFA[pat.charAt(0)][0] = 1;
int X = 0; // 初始化重启状态为0
for (int j = 1; j < M; j++){ // 构造DFA数组过程
for (int c = 0; c < R; c++)
// 状态j遇到字符c不匹配时,把重启状态X遇到字符c到达哪个状态赋值DFA[c][j]
DFA[c][j] = DFA[c][X];
DFA[pat.charAt(j)][j] = j + 1; // 匹配成功, 状态向前推进
X = DFA[pat.charAt(j)][X]; // 部分已经成功匹配字符串中增加了pat.charAt(i), 需要更新重启状态X,即它们的最长重叠字符会发生变化
}
}
- 我们已经计算出了DFA,下面是利用DFA来搜索文本的算法 - 结合书本P498 图5.3.7理解:
public int search(String txt) {
int M = pat.length();
int N = txt.length();
int i,j;
// pat 的初始态为 0 - 模拟有限状态机运行
for (i = 0,j = 0; i < N && j < M; i++) {
// 当前是状态 j,遇到字符 txt[i],
// pat 应该转移到哪个状态?
j = dp[txt.charAt(i)][j];
// 如果达到终止态,返回匹配开头的索引
if (j == M) return i - M;
}
// 没到达终止态,匹配失败
return N;
}
- 结合上面两部分就可以得到具体的KMP算法啦! ( 具体参考书籍上的算法哦
总结
- 要使用KMP算法进行匹配,重要的是求出 DFA 数组,而要求出正确的得到DFA数组,格外需要关注(难理解)的是重启状态X和重启状态X的转移,即为每次发生匹配时,模式 和 pat[1..j]的最长公共前缀。只要得到了DFA数组之后,模拟有限状态机运行就可以进行匹配操作了。
- 对于长度为M的模式字符串和长度为N的文本,KMP查找算法访问字符串不会超过 N + M个。
- 就算书上所提的一样,KMP算法为最坏情况提供的线性级别运行时间保证的一个理论成果,在实际运用中,它比暴力算法的速度优势并不明显
参考资料
[1] https://judes.me/tech/2016/04/10/kmp.html
[2] https://book.douban.com/subject/19952400/discussion/59623403/
[3] https://zhuanlan.zhihu.com/p/83334559