这两种算法都是字符串匹配算法,通俗来说,就是得到子串在主串中的位置。
BF算法:
BF算法是普通(简单)的查找算法,就是我们常说的暴力破解法。
基本思想:
- 将目标串(主串)S的第一个字符与模式串(子串)T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;
- 若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。
图示:
分析:
从BF算法的思想可以得到他的时间复杂度为O(m*n),这是由于i指针一直要回退,但是有些情况不需要回退,就如上述图示情况,回退之后和子串T还是不匹配,所以这就导致了多次毫无意义的比较,浪费了大量时间。虽然这种方法简单易懂,但是时间复杂度太高,不适合在大规模的数据量下使用。
代码详解(c++):
```
#include<iostream>
#include<string>
using namespace std;
int BF(const string& S,const string& T,int pos)
{
int len_s = S.size();
int len_t = T.size();
int i = pos; //主串的位置
int j = 0; //子串的位置
while (i < len_s && j < len_t)
{
if (S[i] == T[j]) //匹配
{
i++;
j++;
}
else //失配
{
j = 0; //j归0
i = i - j + 1; //i回退
}
}
if (j >= len_t) //子串完成匹配,则找到
{
return i - j;
}
else
{
return -1;
}
}
int main()
{
string S;
string T;
int pos;
cin >> S;
cin >> T;
cin >> pos;
cout << BF(S,T,pos) << endl;
return 0;
}
```
KMP算法:
KMP算法是对BF算法的改进,主要是改进了BF算法中,i指针回退的缺点,从而提高效率。
基本思想:
- i不回退,但是j退到相应的位置k;
- 找到位置k后,比较主串i位置和子串k位置的字符,重复BF算法的步骤即可;
由此可见,KMP算法的核心及难点就是找到j回退的位置k
算法核心yi’xi(找j回退的位置k):
1、那么j应该回退多少呢,回退到那个位置呢?那我们就用我们人的思维来考虑j回退后的位置;
由上述推论可得出找到j回退后的位置k的方法:
在子串T中,在匹配成功的“子串”中找到两个最长的相等的前缀子串和后缀子串,两个子串有如下特点:
- 一个子串以0下标作为开头;
- 另一个子串以失配前的最后一个字符作为结尾。
所以这两个最长的相等的真子串的长度就是k会退后的位置。
2、用我们人的思维推出了找到j回退后的位置k的方法,那么我们如何将这种方法应用到代码中,让计算机识别这种方法呢?
因为子串T的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置j对应的k,所以用一个数组next来保存,next[j] = k表示当 S[i] != T[j] 时,j指针的下一个位置。
next数组的实现:
- next数组中,next[0] = -1,next [1] = 0; next[0] = -1,这是因为如果第一个字符就失配,则需要i++,j不需要移动;next [1] = 0,这是因为第二个字符之前就只有一个字符,j必须回到0位置重新匹配。
- 当T[k] == T[j]时,next[j+1] = next[j]+1; T[0 ~ k] == T[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。
- 当T[k] != T[j]时,k = next[k]; 此时T[k] != T[j],k继续回溯前缀(此时的k就相当第一次失配时的j),相当于重复k = next[j]的过程。
static int *Git_next(const string& T) //创建next数组 { int len = T.size(); int *next = new int[len]; next[0] = -1; next[1] = 0; int j = 0; int k = -1; while (j < len-1) { if (k == -1 || T[k] == T[j]) //next[j+1] == k+1 { next[++j] = ++k; } else { k = next[k]; //k继续回溯前缀(此时的k就相当第一次失配时的**j**),相当于重复k = next[j]的过程。 } } return next; }```
3、如上方法,看似已经很完美了,但是还存在一定的瑕疵
- 如图所示,如果再j = 3处失配,则k = next[j] = 1,如果j回溯到k位置,但T[k] = b,仍然失配;
- 这就可以看出此回退是无效的,k还得继续回退,即k = next[k],所以我们就让k一次回退到位;
- 重新计算每一个位置j对应的k的最终回退位置,再用一个数组nextval来保存,**nextval[j] = nextval[next[j]];**表示当 T[j] == T[T[j]] 时,j指针的下一个位置。
所以我们将nextval数组里保存的值就叫修正后的 k,即就是j的最终回退位置,修正后的代码如下:
static int *Git_nextval(const string& T)
{
int len = T.size();
int *next = new int[len];//初次k值数组
int *nextval = new int[len];//修正后k值数组
next[0] = -1;
next[1] = 0;
int j = 0;
int k = -1;
while (j < len-1)
{
if (k == -1 || T[k] == T[j]) //next[j+1] == k+1
{
next[++j] = ++k;
}
else
{
k = next[k]; //k继续回溯前缀(此时的k就相当第一次失配时的**j**),相当于重复k = next[j]的过程。
}
}
nextval[0] = -1;
for (int i = 1; i < len; i++)
{
if (T[i] == T[next[i]]) //T[i] == T[k],这样导致回退后的位置k不是j的最终回退位置。
{
nextval[i] = nextval[next[i]]; //一次性回退到位
}
else //T[i] != T[k]
{
nextval[i] = next[i]; //说明k已回退到位
}
}
delete[]next;
return nextval;
}
算法分析:
KMP算法相比于BF算法最主要的一点就是i不需要回溯了,它的时间复杂度可以达到O(m+n)。
它的核心其实就是next数组,只要理解了next数组KMP算法也就算是解了。
有些人一想到KMP算法就会觉得很难,就害怕了,越只要害怕就越不想去搞懂它,所以这个问题就一直得不到解决,我之前也是这样。但是这一次我硬着头皮,花了两天时间,终于搞懂了KMP算法,只要静下心,下功夫,就没有解决不了的问题。
如果想了解详细代码请看我的GitHub:https://github.com/dong1102/Programming-Exercises/blob/master/BF和KMP算法.md
推荐一篇讲KMP算法的文章,写的还不错https://www.cnblogs.com/yjiyjige/p/3263858.html。