现在有这样一个问题:
有一个文本串S,和一个模式串P,要查找P在S中的位置,如何查找?
用暴力匹配的思路,假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:
- 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
- 如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
按以上思路很容易就可以实现该算法,遗憾之处在于此算法的时间复杂度较高,为O(N*M),N,M分别为模式串和文本串的长度。那有没有更快的算法呢?当然是有的,其中之一就是KMP算法。
KMP算法相比暴力算法的优化之处以及原因,在这个文章第二段可以看到;KMP算法的流程和原理,在第三段也解释了,这篇文章主要记录下自己学习KMP算法的一些心得。
对于next数组,网上解释的普遍不是很明白,这里记录下自己的理解。
这个文章的3.2的解释比较清楚了,next数组来自于前缀后缀最长公共元素,next数组依赖这个得到。
前缀后缀最长公共元素(以下简称最长公共串)的含义
其实对于一个给定字符串来说,公共串的求法很简单:把一个串的所有前缀后缀列出来,相同的前缀后缀中最长的那个就是最长公共串。比如字符串ababa,前缀是:a,ab,aba,abab,后缀是a,ba,aba,baba,相同的最长的是aba,所以最长公共元素的长度就是3。
这个信息在KMP算法里有何意义呢?如果我一个模式串的前五个字符就是ababa,刚好都匹配了文本串,然后下一个字符失配了,这个时候如果按照暴力算法,需要把模式串往前移动一个字符;这个信息在此时就用得上了:最长公共元素意味着,ababa前五个字符组成的字符串,移动后最多只能有三个字符匹配。
为什么呢?ababa这个字符串,不同长度前缀分别是:a,ab,aba,abab;不同长度后缀分别是:a,ba,aba,baba。如下图所示,移动不同的字符数,就分别对应了不同长度前后缀的比较(比如移动两个字符,就对应了长度为3的前后缀的比较)
所以至少需要移动两个(已匹配的长度5-最长公共元素长度3)字符,才可能出现匹配的情况。
从这个例子可以很明显看出来,每次失配后,需要移动的字符数=已匹配的长度-最长公共元素长度。而在next数组中,为了方便使用,将最长公共元素长度数组向右移动一位,然后next[0]赋值为-1即可。
实现过程中的问题
(1)next数组的求解
对于next数组的求解,可以参考这个知乎问题中答主恶劣天气的回答,根据这个回答写出计算过程如下(needle是模式串)。
next[0] = -1;
int k = 0;
for (int i = 1; i < next.size();) {
if (i == 1) {
next[i] == 0;
i++;
}
while (k >= 0 && i < next.size()) {
if (needle[i - 1] == needle[k]) {
k++;
next[i] = k;
i++;
}
else { k = next[k]; }
}
if (i < next.size()) {
k = 0;
next[i] = 0;
i++;
}
}
主体思路就是对模式串中每一个字符,循环求它的每个next[i],在循环中,每个next[i]再在一个while循环中求解,如果下一个字符相同,next值增加1,继续求下一个next[i];如果不同,则多移动一些,再尝试求next[i],一直移动到开头还找不到,则说明开头字符到当前位置字符对应的字符串最长公共元素长度为0,故这时next[i]=0。
为什么i=1时要特殊判断?因为没有这个特殊判断,按照后续的判断逻辑,会走到第一个if分支,next[1]就会求解出错。
这种写法逻辑是OK的,但相比标准求解的代码还是繁杂了些:
next[0]=-1;
int k=-1;
int i=0;
for(;i<next.size()-1;) {
if(k==-1 || needle[i]==needle[k]) {
i++;
k++;
next[i]=k;
}else {
k=next[k];
}
}
标准求解代码优化的点:
1.下一个字母匹配的情况和所有字母完全不匹配的情况合入到一个if分支;
2.基于第一点,求next[1]的代码合入到一般next[i]的求解过程;
优化之后的代码看着简洁了很多,但其实也考虑了各种边界条件的情况,确实是很优秀的代码了。
(2)字符串匹配的代码,haystack是字符串,needle是模式串
int pos = 0;
for (int subPos = 0; pos < haystack.length();) {
if (subPos == needle.length()) { return pos - subPos; }
else if (subPos == -1) {
pos++;
subPos = 0;
}
else if (haystack[pos] == needle[subPos]) {
pos++;
subPos++;
}
else if (subPos >= 0) { subPos = next[subPos]; }
}
return -1;
以上的代码逻辑很清晰,4个分支分别代表:已经匹配到模式串;已经移动到模式串开头,从字符串下一个字符继续匹配;下一个字符匹配;下一个字符失配。看起来所有情况都考虑到了,但实际上还有未考虑到的情况:如果模式串和字符串完全一样,将会返回-1。看看正确的写法:
int pos = 0, subPos = 0;
int slen = haystack.length();
int nlen = needle.length();
for (; pos < slen && subPos < nlen;) {
if (subPos == -1 || haystack[pos] == needle[subPos]) {
pos++;
subPos++;
} else if (subPos >= 0) { subPos = next[subPos]; }
}
if (subPos == nlen) { return pos - subPos; } else { return -1; }
将是否匹配的代码放在循环之外,避免了之前代码的漏洞。还有值得一提的是先用一个int变量存长度,看起来无关紧要,但因为中间会出现-1和字符串长度比较,如果没有这个赋值而是直接比较,length函数返回的是size_t,与-1比较,类型转换时-1会变成INT_MAX,导致比较结果错误。
结语
KMP算法看起来不难,但实际实现时有很多要注意的细节,自己实际去写一遍会有更多收获