字符串模式匹配之KMP算法的理解和应用
模式串匹配过程
设匹配串为s
,对应的移动指针i
;模式串为p
,对应的移动指针为j
。
补素的匹配过程
下面是补素的匹配过程,且模式串匹配成功,前两次比较,遇到p中的字符c
时,就匹配失败了,模式串回退到0,同时匹配串移动到下一个位置开始匹配:
第一次比较:s[0] == p[0],s[1] == p[1], s[2] != p[2],比较3次
匹配串s:[a, a, a, a, c]
模式串p:[a, a, c]第一次比较:s[1] == p[1],s[2] == p[1], s[3] != p[2],比较2次
匹配串s:[a, a, a, a, c]
模式串p: [a, a, c]第三次比较:s[2] == [p2],s[3] == p[1],s[4] != p[2],比较3次
匹配串s:[a, a, a, a, c]
模式串p: [a, a, c]
优化的匹配过程
从上帝视角来看,可以将上面的过程优化成如下的比较过程,要吧明显知道,在前两次比较时,遇模式串的字符c
时,可以通过右对齐模式串,更快的得到匹配结果:
第一次比较:s[0] == p[0],s[1] == p[1],s[2] != p[2],比较3次
匹配串s:[a, a, a, a, c]
模式串p:[a, a, c]
通过比较,发现模式串字符`c`所在的位置不同,匹配失败。第二次比较:s[2] == p[1],s[3] != p[2],比较2次
匹配串s:[a, a, a, a, c]
模式串p: [a, a, c]
此时字符c所在的位置不相同,但由于s的子串[a, a, a](下标是[0,1,2])后缀与模式串的前缀相同,因此可以右移模式串,对齐如下:0 1 2
[a, a, a][a, a, c]第三次比较:s[3] == p[1],s[4] == p[2],比较2次
匹配串s:[a, a, a, a, c]
模式串p: [a, a, c]
同理第二次的比较过程,得到如下的对齐:0 1 2 3
[a, a, a, a][a, a, c]
从优化后的过程,可以得到如下的基本事实(当遇到不匹配的字符时,对应匹配串中的最右下标i
、最左下标k
,模式串的下标j
)
- 如果匹配串与模式串的前缀子串成功匹配时,必然是匹配串的后缀子串与模式串的前缀子串匹配。
- 不可能出现在匹配串的区间
[k, i)
中,存在一个子串,完全匹配模式串。(反证法,如果存在,则匹配串s不可能迁移到i
位置) - 不可能出现在匹配串的区间[k+m, i]中,存在一个比已经匹配成功的模式串的子串[0,j]区间,更长的匹配子串,而且最长的匹配串的区间只可能为[k+1, i],且
s[i] 与p[j-1]对齐
。(显然s[i] != p[j]时,最好的情况是区间s[j+1, i]的子串与模式串的前缀匹配,相当于模式串整体右移一个字符,此时匹配的子串长度就是j) - 一旦匹配串的当前字符与模式串不匹配时,找到下一个可以匹配的子串的方法,是找到匹配串的后缀与模式串的前缀的公共子串。
KMP算法的思想
通过优化后上帝视角
匹配过程,可以得到一个结论,要想快速确定模式串的匹配性,可以在遇到不匹配的字符时,记下标为i
,保持匹配串的当前位置不变(匹配串的后缀右边界),右移模式串,找到下一个、最长的公共子串(匹配串的后缀与模式串的前缀相同的子串
),并从匹配串的i
位置继续匹配即可。
现在的问题是如何确定后缀前缀最长公共子串
,即要右移模式串的距离?
实际上这里有一个角色转换思想,由于是在遇到不相等的字符时,才需要找下一个最长的公共子串,此时模式串与匹配串的前缀肯定是相同的,找匹配串的后缀与模式串的前缀的公共子串,就可以转换为找模式串的后缀与模式串的前缀的公共子串
,一旦知道了公共子串的长度,就可以基于这个长度右移模式串,对齐到相同的长度!!
同时考虑到新找到的公共子串来自于模式串,而且最后一个字符只有与p[j]
不相同时,才有可能和s[i]
字符相同,因此在基于模式串构建后缀前缀公共子串的信息时,实际的子串的最后一个字符应该是恰好与p[k]不同的字符
!!
KMP算法的优势
时间复杂度
如果模式串和匹配串中存在大量的重复子串,那补素算法的时间复杂度为O(m*n),但模式串的时间复杂度接近O(m+n),这是由于匹配串(主串)不需要回溯,同时模式串通过回跳表快速对齐,减少了大量的回溯次数,因此KMP算法的优势是显而易见的;但一般情况下,主串和模式串是无序的,因此即使是补素的算法,时间复杂度也能接近O(m+n)。
空间复杂度
KMP空间复杂度O(m),取决于模式串的nexts数组的存储,而补素的算法为O(1)。
因此在实际使用中,如果存储是瓶颈,则需要具体情况进一步优化KMP。
应用
KMP算法适用于大规模字符串匹配问题,例如:
文本搜索:快速定位特定模式在文档中的位置。
生物信息学:DNA序列比对。
网络协议分析:检测特定数据包模式。
算法实现
public int[] constructNexts(String str) {int pre_j = -1; // 模式串的指针int cur_i = 0; // 匹配串的指针char[] cs = str.toCharArray();int[] nexts = new int[str.length()];while (cur_i < str.length() - 1) { // 最后一个字符不关心,不论它是否在某个公共子串中,都需要进行一次比较if (pre_j == -1 || cs[cur_i] == pre_j) {// 注意这里是在cur_i处登记公共子串的长度,如此在查找匹配串的过程中,遇到不匹配的字符时,// 就直接通过nexts[j],使模式串的前一个公共前缀子串+下一个不等字符,恰好对齐到原匹配串的当前位置。// 当pre_j==-1时,此时比较的模式串的第一个字符,它的公共串的长度为0nexts[cur_i + 1] = pre_j + 1;// 比较下一个字符cur_i++; pre_j++;} else {// 当遇到不匹配的字符时,需要令pre_j回跳到,以子串[0, pre_j]为基准时记录的最长公共前缀,// 并期望cs[nexts[pre_j]] == cs[cur_i],那么就可以基于新的公共子串继续匹配cs[cur_i]以及之后的字符。pre_j = nexts[pre_j];}}
}public boolean match(String str, String pattern) {int[] nexts = constructNexts(pattern);char[] scs = str.toCharArray();char[] pcs = pattern.toCharArray();int i = 0; // 匹配串str上的指针int j = 0; // 模式串pattern上的指针while (i < str.length() && j < pattern.length()) {if (j == -1 || scs[i] == pcs[j]) {// j == -1时,只可能是由于scs[i] != pcs[0]时出现,因此// 需要同时增大i,j,从匹配串的下一个字符开始全新的匹配过程。i++; j++;} else {// 基于nexts数组,右对齐模式串j = nexts[j];}}// 只有当模式串走完时,说明匹配成功return j >= pcs.length;
}