KMP算法解释
背景
KMP算法(Knuth-Morris-Pratt算法)是一种用于字符串匹配的高效算法,由Donald Knuth、Vaughan Pratt和James H. Morris在1977年共同提出。KMP算法的主要优点是在进行字符串匹配时,它避免了重复检查已经匹配的部分,从而提高了匹配效率。
基本思想
KMP算法的核心思想是利用已经匹配的信息来加快匹配过程。当在主串(original)中发现一个字符与模式串(target)中的字符不匹配时,不需要回退主串的指针,而是利用部分匹配表(pi)来决定下一步应该跳转到模式串中的哪个位置继续匹配。
KMP算法引出的子问题:如何求得部分匹配表(pi)
什么是部分匹配表?(表格请竖着看)
索引 | 字符(target) | π[i] |
---|---|---|
0 | A | 0 |
1 | B | 0 |
2 | A | 1 |
3 | B | 2 |
4 | C | 0 |
5 | A | 1 |
6 | B | 2 |
7 | A | 3 |
8 | B | 4 |
9 | A | 3 |
不理解KMP可以,我们不妨先理解子问题:
前缀函数表(也叫部分匹配表、失配函数)记录了模式串中每个位置之前的最长相同前缀和后缀的长度。
- 前缀(Prefix):字符串从开始位置到某一位置之间的子串,不包括最后一个字符。例如,对于字符串 “ABC”,它的前缀包括 ““、”A”、”AB”。
- 后缀(Suffix):字符串从某一位置到末尾之间的子串,不包括第一个字符。例如,对于字符串 “ABC”,它的后缀包括 ““、”C”、”BC”。
比如:
索引为8的地方:
0~3为”ABAB”,是长度为4的前缀,5~8为”ABAB”,是长度为4的后缀,而且前缀后缀的字符相等,经观察可以发现相等的前缀后缀的字符最大长度就是4,所以π[8]=4
解决子问题的思想
初始状态:pi[0]=0;
那接下来肯定是比较A和B,发现没有共同前缀后缀,currentFitLength = 0
再看”ABA”有共同前缀后缀吗?有长度为1的”A”。那我怎么知道这个是最长的同前缀后缀?因为pi[1]=0;那么pi[2]顶多是0+1=1,不可能超过这个值。又比如当我想要通过pi[8]=4来得到pi[9]是多少?我只知道pi[9]<=4+1,因为最好情况就是str[9]==str[4],此时pi[9]=4+1=5。(当然上面的例子不符合最好的情况)
那理解上面的话之后,我知道pi[8]=4后,我怎么知道pi[9]=3?我们可以看到当前的currentFitLength=4。那我们不妨将str[9]翻折到str[4]的位置上。为什么可以这么做?因为str[0~3]和str[5~8]的4位都是一样的啊,那么此时问题就变成求”ABABA”的最长公共前后缀,显然答案是3,因为pi[3]=2,也就是str[0~1]和str[2~3]是一样的,那么此时我们就要比较str[2]和str[4],发现一样,那么pi[4]=pi[3]+1=3。
请注意”翻折”(对应代码:currentFitLength = pi[currentFitLength - 1];)可能不止有一次,可能是两次三次……
最后一步:实现kmp算法
基本思路是匹配不成功时将target向右移动,但移动几格是有技巧的:
- pi[currentEqualLength - 1] == 0时移动1格
- pi[currentEqualLength - 1] > 0时移动pi[currentEqualLength - 1]格,因为target前缀后缀有相等的,通过跳过这么多格来免去不必要的比较过程,比如target=”ABABC”(pi:00120),当我们比到第五位(str[4])时,发现和original的一部分(比如…ABABA)不一样,此时target向右移动两格,因为target[2~3]==original[n+2~n+3],那么根据pi[currentEqualLength - 1]=pi[3]=2,我们知道target[0~1]也==original[n+2~n+3],当然移动两格。
注意”-1”是为了抵消for循环的影响:targetMoveStep = targetMoveStep + currentEqualLength - 1;