朴素的BF算法
BF算法即暴力求解字符串匹配的算法
面对这样两个字符串,
BF算法就是用两个指针,一个i,一个j,分别从s和t的开始位置开始依次匹配
当遇到s[i] == t[0]的时候,此时有可能字符串匹配,需要进行检查
于是从i开始,依次比较i后面的元素和t中的元素,
如果恰好完全相等(次序,字母都相同),则返回i的下标即可
如果遇到了s[i] != t[j],则说明此处不匹配,i需要回退到比较前的i的下一个位置,j需要回退到0,重新开始进行比较
这种算法的时间复杂度是O(m*N) ,N为S串长度,m为T串长度
这种算法的优点是算法思路非常简单,实现起来很容易,库里面的strstr函数就是使用的BF算法
缺点也很明显,时间复杂度稍微有点高
KMP算法
为了提高字符串匹配的效率,D.E.Knuth、J.H.MorTis和V.R.Pratt三位大佬提出了KMP算法
我们发现,限制BF算法的效率最大的障碍,在于回退,回退重复了大量的相同步骤
所以,想要提升字符串匹配的效率核心的问题在于如何避免或者减少下标回退的问题。
KMP算法中将这个问题处理成了让i不回退,让j尽量少的回退
样例引入
我们来看这样一个字符串
主串是 a b c a b a b c a b c
子串是 a b c a b c
当i 走到图中a,j 走到图中c的位置
第一次出现了不匹配,此时注意,
我们可以发现,主串a前面出现的ab在子串c前面也出现过,并且在子串开头也出现过
如果我们把j回退到子串中下标为2的位置,是不是相当于,ab已经匹配好了,开始匹配第三个位置
于是这就减少了回退的距离
原理分析
此时只是无数例子中的其中一个例子,如何将这种规律普遍化呢?
我们来分析一下规律,是不是有些小伙伴以为j的回退与主串中的ab有关系,实际上是没有关系的,只与子串中的ab有关系
为什么这么说呢?
来,我们这么想,当i走到a位置,c走到j位置的时候,i前面的串和j前面的串一定是匹配的,不然j不可能可能走到这里,所以主串中的ab,一定和j前面最近的ab是相同的,
所以回退到下标为2的位置与主串没有关系
只与子串中开头出现了ab有关系
因此,我们继续研究子串
我们需要寻找的是j前面的子串和开头位置的子串相同
也就是,我们需要寻找这样的串
t[0]……t[x] = t[y]……t[j-1],即以t[0]字符开始,t[j-1]字符结束的相同串
这样,出现匹配失败后,我们j就可以回退到下标为x+1的位置
看完原理后,读者可以尝试手动求一下下面图中的两个题下标练练手(虽然给出了答案,但是还是先手算一下吧)
原理实现思路
j尽量少回退的原理我们懂了,那如何实现j尽量少回退呢?
首先我们引入一个next数组,用来存放j应该回退到的位置
我们特殊设置next[0] = -1 ,next[1] = 0,这两个位置情况特殊,找不到两个串
问题就转变成了如何求next数组
我们计next[j] = k
当我们跳转到k的时候,我们知道t[0]……t[k-1] == t[j-k]……t[j-1]
我们分成两种情况讨论(sub为子串,即t串,src为主串,即s串)
- sub[k] == sub[j]
此时相等的串继续增加,得到sub[0]……sub[k] == sub[j-k]……sub[j]
因此next[j+1] = k + 1- sub[k] != sub[j]
此时k可以继续循环k = next[k],找到一处合适的k,满足条件后走情况一即可。- 还有一种特殊情况,当k 循环到了 next[0] = -1的时候,next数组中-1的下标肯定是不能继续访问的,
此时,说明在子串中找不到符合要求的两个串,直接下标给0即可,此种情况放到情况一种恰好满足,
当然也可特殊写一下
开始匹配
next数组求出来之后,我们就已经解决了KMP算法的核心问题,接下来只需要进行简单的匹配就行了
按照前面说的,i不会退,j尽量少回退的原则进行匹配即可。
遍历主串,
当src[i] == sub[j] 的时候,++i, ++j
当src[i[ != sub[j] 的时候,j = next[j]重新比较即可
当j到了-1的时候,也就是子串的第一个字符都不匹配,那主串这个字符不可能匹配,直接++i,++j即可,可归类到情况一种
时间复杂度分析
在KMP算法中,i并未回退,j近似未回退,也就是相当于遍历了两个串
时间复杂度为O(m + N)
为什么BF算法沿用至今
但在一般情况下暴力匹配算法执行时间也近似为O(m+n),O(M*N)是最坏情况,但是最坏情况出现的概率比较小,毕竟我们寻找的子串大概率是一组无序的字符组成的,而不是有很多重复。
代码
vector<int> get_next(string sub)
{
int n = sub.size();
vector<int> next(n);
next[0] = -1, next[1] = 0;
for (int i = 2; i < n; ++i)
{
int k = next[i - 1];
while (1)
{
if (k == -1 || sub[i - 1] == sub[k])
{
next[i] = k + 1;
break;
}
else
{
k = next[k];
}
}
}
return next;
}
int KMP(string src, string sub, int pos)
{
int n = sub.size();
vector<int> next(n);//存放next数组
next = get_next(sub);//求next数组
int i = 0, j = 0;
while (i < (int)src.size() && j < (int)sub.size())
{
if (j == -1 || src[i] == sub[j])
{
++i;
++j;
}
else
{
j = next[j];
}
}
if (j == sub.size())//找到了
{
return i - j;
}
if (i == src.size())//找不到
return -1;
}