刷leetcode时了解到了这个算法,断断续续学了几天,今天试着写下错了好多遍终于过了。虽然还是一知半解,但也算知道点东西所以先写下来吧,应该对一些人有帮助,以后理解更深了再补充吧。
基本定义(概念都是随着写随着编的,有不准确也没办法,为了写着方便)
- 模式串:要找的
- 源串:从这里找
- 已匹配串:。。。
- 不匹配字符:。。。
- 前匹配:前多少个字符模式串和源串是一样的。
- 前缀串:不包含后k个字符的串(k>=1)
- 后缀穿:不包含前k个字符的串(k>=1)
- 反前缀: 以某个字符为分割点逐一向前面取字符(不包含第一个),最后该字符串刚好是前缀串。
- 缀串: 前缀串或后缀串
- 最长同缀串:已匹配串中前缀和后缀两个字符串相等且长度最大的字符串
1 算法来源
旧的字符串匹配算法匹配效率太低,O(MN)的。为了提高效率有人想到了一种巧妙的办法。O(MN)那个算法中,如果匹配很少就出现不匹配的话,那么匹配速度就比较快些。但如果已经匹配很长出现不匹配的情况,那么匹配速度就慢。所以如果能够将不好的情况处理好使他也能匹配快些的话就好了。
这两者的区别 就在于已匹配串长度不同。
2 算法思路
当出现不匹配的情况时,要把模式串往右移动继续和源串进行匹配。但如果想要匹配速度快点,就 得想法在安全的情况下多移动几步。
KMP
可以注意到当将模式串向右移动的时候,如果在移出上次源串已匹配字符范围前又发生了模式串和源串前匹配,那有这次匹配上的部分刚好是上次源串已匹配字符范围内的后缀部分。 而这样的后缀可能有很多个,为了更加安全,可以取长度最大的那一个,这样移动的次数最小。
移动的次数是多少呢?两个已知条件:已匹配串的长度设为n, 最长同缀串的长度m. 容易知道是n-m.
所以模式串 向右移动时可以移动n-m个位置,而不是移动1,这样会更快些,已匹配串越长,最长同缀穿越短,移动的越快,问题似乎变了,已匹配串的长度容易知道,但是最长同缀串长度是多少这个还不清楚。
求最长同缀穿长度
最长同缀串是 出自模式串的,又随着在模式串中不匹配字符出现的 位置而变化, 因此,
最长同缀穿长度需要存放到一个数组中。
这里就不太好写了,只能写个例子体会下过程。写具体的例子局限性又太高。所以写尽量通用的例子吧
设i=0, j=1. i代表最长前通缀串的尾标。j代表最长后通缀串的尾标。已有L[0]=0,模式串为 P
因为前缀串的起始位置为0,最后为i,所以前缀串的长度为i+1.
(1) 如果P[i] = = P[j],那么L[j]=i+1; //这样写是因为已经保证了i-1, j-1; i-2,j-2; 等等都是对应相等的
(2) 如果P[i]!=P[j], 那么
如果P[j]前面没有已匹配的部分,说明此时i=0,此时L[j]=0, j++即可
如果P[j]前面有已匹配的部分,要说明的是此时P[j]前面的和P[i]前面的是一一对应的。此时我想在[0,i]的前缀串中找P[j]的最长反前缀并且后一个字符刚好是P[j]的,那么我们就找到了这样一个点,刚好P[I]==P[J], 就可以得到L[j]=i+1了。
所以因为前和后缀串是一一对应的,所以可以只在前面中找. 因为无法直接找到这样一个地方,但是可以逐渐的找,可以先找到位置 i的最长反前缀串的位置,为[0,L[i-1]-1], 因此L[i]就是需要和P[j]对比的位置.如果相等,那么就又到了上面的(1)处的逻辑。 如果不等,那么继续(2)的逻辑。直到L【i''】为0。
具体使用KMP
已经有了最长同缀数组,接下来就是使用它,值得一提的是,思考过程 跟构建KMP数组是差不多的,只是要考虑什么时候 代表找到了然后返回找到的位置。 如果没找到,返回-1.
下面是代码
void buildKmpArr(vector<int>& kmpArr, string& needle){
if(kmpArr.size()<2){
return;
}
int i=0;
int j=1;
while(j<kmpArr.size()){
if(needle[i]==needle[j]){
kmpArr[j]=i+1;
i++;
j++;
}else{
if(i==0){
kmpArr[j]=0;
j++;
}else{
i=kmpArr[i-1];
}
}
}
}
int strStr(string haystack, string needle) {
if(needle.size()==0){
return 0;
}
vector<int> kmpArr(needle.size());
kmpArr[0]=0;
buildKmpArr(kmpArr, needle);
int idx{};
for(int i=0;i<haystack.size();){
if(needle[idx]==haystack[i]){
idx++;
i++;
if(idx==needle.size()){
return i-needle.size();
}
}else{
if(idx==0){
i++;
}else{
idx=kmpArr[idx-1];
}
}
}
return -1;
}
leetcode执行结果:
通过 | 4 ms | 7.4 MB | C++ | 2021/09/24 20:42 | 添加备注 |
遗留的问题
1、当遇到P[i]不匹配时,此时 是先看i是不是0,如果不是,会令P比较的位置变为L[i-1],这样写能够确保是第二长的反前缀串吗,因为第二长的才能确保是安全的移动。
20210926 答:kmpArr数组中元素的意义是到当前索引位置为止最长的通缀串。先不考虑存不存在,令i=L[i-1]本质上是舍弃了了后缀串去从前缀串中找机会的过程。因为从后缀串中找是没有意义的,他后面永远都是未匹配的那个字符。
第二长的同缀串假设有,那么第二长通缀串应该在第一长前缀串的前面和后缀串的后面。因为第二长的通缀串后面那个串后面的字符也同样是未匹配的,所以只能从第二长的前缀串中找机会看有没有。但是因为第一长的前缀串比较长,如果用第二长的前缀串即使找到了这个机会,又如果第一长的也有这个机会呢,那么不就相当于多移动了吗,这样是不安全的,始终用第一长的前缀串则没有问题。