KMP想必是每个初学算法的人敲来的一记重棒,代码写出来十分的简洁,但是其中的思想不能说高深莫测,只能说是晦涩难懂(bushi)。啃了两天的KMP,终于是对其有了一个相对浅显易懂的理解:
1.暴力解法与KMP的作用
KMP是用于高效匹配一个字符串中的目标串的算法,如果按照按照常规暴力匹配的话
//haystack为原串,needle为要查找的串,n,m分别为其长度
for (int i = 0; i <= n - m; i++) {
int j = 0;
// 检查从 haystack[i] 开始的 m 个字符是否和 needle 匹配
while (j < m && haystack[i + j] == needle[j])
j++;
// 如果完全匹配,返回起始索引 i
if (j == m)
return i;
}
// 如果没有找到匹配,返回 -1
return -1;
那必然是O(N*M)的复杂度对吧,基本就是超时的命。
但KMP能做到O(N+M)
所以就有了KMP这一个算法来实现这一需求,STL里面的find()就是基于KMP实现的,只不过是KMP PRO++ MAX,效率更高。虽然我们完全可以使用find(),但建议大家还是先能熟练写基础KMP再使用,一是锻炼自己,二是可以自己创造出特殊需求的KMP。
2.最长公共前后缀
前缀(后缀):
假如有串ABCD,就有
前缀 | A | AB | ABC |
---|---|---|---|
后缀 | D | CD | BCD |
前缀,后缀不包括本身!顺序一致!(都是从左到右数)
显而易见的,ABCD没有公共的前后缀,所以其最长公共前后缀为0
我们换一个:ABAB
前缀 | A | AB | ABA |
---|---|---|---|
后缀 | A | AB | BAB |
又显而易见的发现,其最长公共前后缀为2(AB)
我们一般用next来存储一个字符串的最长公共前后缀,以ABAB为例子,比如next[2]代表ABA的最长公共前后缀(为1,最长公共前后缀为A)
接下来我们看为什么最长公共前后缀能实现高效搜索
3.KMP的原理
一切的算法都是为了优化,KMP就是优化了无效的搜索
比如我们要在AABAABAAC中搜索AABAAC
从头开始匹配,发现 AABAABAAC 和 AABAAC 中B和C不对了
按照暴力来的话,会重新从第二位开始匹配,KMP优化的就是这里.
你看,已经匹配成功的AABAA next是不是为2?也就是AA 和 AA是相同的!!!
那为什么不能从AA开始搜索呢?这不就省去很多无用搜索吗!!!
(大致意思如图)
有人可能会问,这样的Blink是不是太夸张?AA和AA之间不可能会有答案吗?直接跳到了AA?
还真不可能!
如果通过字母分析会过于抽象,我画了个图:
块2,块3你可以又无限分,最后平移的结果仍是最长前缀平移到后缀处才有可能匹配(建议自己画一下)
4.利用next搜索
这里先假设我们求得了next数组(next较为难求)
那只需要按图索骥即可,每次搜索出错后便退回此时的next的值重新匹配
for(int i = 0,j = 0;i<n;i++){ //枚举
while(j != 0 && needle[j] != haystack[i])
j = next[j-1]; //发现不对,尝试回退
if(needle[j] == haystack[i])
j++; //记录匹配了几个
if(j == m)
return i-m+1; //完全匹配,返回答案
}
return -1; //全部起点都枚举完,没找到
}
注意是next[j-1]假如是AABAAC,我们要找的是AABAA的最长前后缀,所以是next[j-1]
j = next[j-1]就相当于回退(原本匹配到AABAA现在重新到AA匹配)
5.next的计算
先贴上代码:
for(int i =0,j = 1;j<m;j++){ //i代表左端,j代表右端
while(i >0 && needle[i] != needle[j])
i= next[i-1];
if(needle[j] == needle[i])
i++;
next[j] = i;
}
if(needle[j] == needle[i])
i++;
next[j] = i;
这一段很好理解,i代表左端,j代表右端,如果新增的(needle[j])仍然匹配,就左边右移一位,总长度+1
但如果不匹配呢?
while(i >0 && needle[i] != needle[j])
i= next[i-1];
我们会把i回退! 也就是左端回退!
但是,有人会想,应该是枚举位数重新匹配啊,如果3位对不上就换2位看看对的上不啊
这样来计算next是没问题,但这样不是很慢吗?又要重新匹配一遍?
于是乎,公共前后缀的奇妙之处来了
新加的一位没匹配上,大概就是这个情况:
块2和块3不匹配,怎么办?回退!
别忘了,前端和后端(绿线部分)也有最大前后缀的!
那么我们为什么不这么匹配呢? (参照之前Blink)
相当于转到了i= next[i-1];
道理就是前端的(最长)前缀等于后端的(最长)后缀!毕竟你前端一定等于后端嘛,再没加新字母的情况下!
那出现不符字母时只需看前端的最长前缀后一位和新的字母一不一样!!
如果不一样,再回退!
所以就有如下代码
for(int i =0,j = 1;j<m;j++){ //i代表左端,j代表右端
while(i >0 && needle[i] != needle[j])
i= next[i-1];
if(needle[j] == needle[i])
i++;
next[j] = i;
}
总代码为
class Solution {
public:
int strStr(string haystack, string needle) {
int n = haystack.size();
int m = needle.size();
int next[m];
next[0] = 0;
for(int i =0,j = 1;j<m;j++){ //i代表左端,j代表右端
while(i >0 && needle[i] != needle[j])
i= next[i-1];
if(needle[j] == needle[i])
i++;
next[j] = i;
}
for(int i = 0,j = 0;i<n;i++){
while(j != 0 &&needle[j] != haystack[i])
j = next[j-1];
if(needle[j] == haystack[i])
j++;
if(j == m)
return i-m+1;
}
return -1;
}
};
希望大家能有帮助!有错误或者模糊的地方还望指正!