如何理解KMP算法

KMP算法是对字符串暴力匹配算法的改进,要理解KMP算法,首先需要了解字符串的暴力匹配算法。

暴力匹配算法

假设有主串“abcabcabcabf”和模式串"abcabf",我们需要逐个比较主串s和模式串p的字符,如果模式串的每个字符都能和主串的某个子串匹配上,则查找成功

 从i=0,j=0开始,逐个比较s[i]和p[j],当i=5并且j=5时,出现s[i] != s[j] 的情况

这时i应该回退到i=1的位置,j回退到j=0,然后再次开始逐个比较

直到最后 i = 11, j = 5时匹配结束

我们发现使用暴力匹配算法,在最坏的情况下,当每次匹配时都是模式串的最后一个字符和主串子串的最后一个字符不一样,直到扫描到主串结尾。假设主串的长度为N,模式串的长度为M,那么时间复杂度为O(MN)。效率不是很高。

暴力匹配的C++代码为:

int ForceSubstrSearch(string str, string pattern)
{//暴力搜索算法
    int n = str.size();
    int m = pattern.size();

    int i, j;
    for (i = 0, j = 0; i < n && j < m; i++)
    {
        if (str[i] == pattern[j])
            j++;
        else
        {//回退
            i -= j;
            j = 0;
        }
    }
    if (j == m)
        return i - m;
    else
        return -1;
}

KMP算法

算法思想

在了解了暴力匹配算法后,我们知道其时间复杂度为O(MN)。

我们在匹配到模式串的最后一个字符时,实际上已经知道了主串的s[0] ~ s[4] 和 p[0] ~ p[4] 是完全一样的,那么我们完全可以利用这一点,通过前面的匹配得到的主串的一些信息,来减少一些不必要的匹配操作,降低时间复杂度,这就是KMP算法的思想。

 接下来要知道前缀和后缀的概念,对于一个字符串ABCD来说,它的前缀就是A、AB、ABC,后缀是BCD、CD、D。前缀和后缀不能是ABCD。

下面观察一下s[0] ~ s[4] 和 p[0] ~ p[4]这个子串abcab

我们可以得到前缀和后缀:

通过观察我们发现ab是abcab的相等前后缀
假设最长相等前后缀不存在,也就是说,主串和模式串已经匹配的部分里,所有的字符都和模式串的首字符不一样,这段子串也就没有再和模式串的任何一个字符比较的必要了,比如下面的匹配,'c ' != 'f' ,对于已经完成匹配的部分 ab 来说,不存在相等的前后缀,那么直接j = 0 ,i + 1就好了;

假设存在最长的相等前后缀,主串和模式串匹配时因为abcab的前缀ab和后缀ab是相等的,并且,最长的相等前后缀就是ab,所以我们其实进行下一次匹配时,i不用动,j回退到 j = 2 ,然后再比较 s[i] 和 s[j] 即可,这就是利用上一次失败的匹配得到的信息来减少不必要的匹配次数。 

 通过观察发现ab的长度是2,而j回退的位置刚好索引为2,也就是说 j 回退的位置索引就是已经匹配过的子串的最大相等前后缀的长度。

所以KMP算法的思想就是当匹配失败时,利用已经匹配过的子串的最大相等前后缀的长度来确定 j回退的位置,而 i 不必回退,而子串的最大相等前后缀的长度存储在next数组中,每当s[i] != s[j] 并且 j > 0时,j 都回退到next[j - 1]的位置,然后继续比较。

求next数组

求next数组可以使用动态规划的思想,进行递推。

next[i]的定义为p[0] ~ p[i] 这段子串中最大相等的前缀和后缀的长度,假设此长度为 j ,假设已经知道next[0]、next[1]、next[2]、next[3]、.... next[i - 1],要求next[i] 的话,可以比较p[i] 和 p[j] (数组下标从0开始,j指向最长前缀之后的一个字符)

如果p[i] = p[j],那么显然最大相等前后缀的长度要增加1;

如果p[i] != p[j],我们观察一下面这张图,我们需要从已经匹配好的子串abcab中找到一个 j 回退的位置,使 p[i] = p[j],j 回退后需要尽可能的大一点。我们知道子串1和子串2是相等的,所以找到最大的 j,应该是在p[0] ~ p[j] 这个子串中找到最大的相等前后缀的长度,而这个前缀正好和子串2的对应长度的后缀是相等的,那么 j 回退的位置刚好应该是next[j - 1]。

 

j 回退为 j = 2 ,然后发现p[i] = p[j],这使 j + 1,然后更新next数组,?的值为3。

 

vector<int> getNext(string p)
{
    //用i指向后缀尾,j指向前缀尾
    //初始化
    int n = p.size();
    vector<int> next(n, 0);
    int j = 0;  //j表示前缀尾,也代表当前正在比较的前缀和后缀的长度, i表示当前后缀的最后一个字符串
    for (int i = 1; i < n; ++i)
    {
        //处理p[i]和p[j]不相等的情况
        while (j > 0 && p[i] != p[j])
            j = next[j - 1];//回退j
        //处理p[i]和p[j]相等的情况
        if (p[i] == p[j])
            j++; //如果前缀尾和后缀尾相等的话,最长相等前后缀的长度+1
        //更新next数组
        next[i] = j; // next[i] 更新为当前最长相等前后缀的长度
    }
    return next;
}

KMP

 有了next数组以后,当发生匹配失败时,只需 j 回退就好,除非 j = 0,没办法回退了,这时 i 需要加1

代码如下:

int KMP(string str, string pattern)
{
    int n = str.size();
    int m = pattern.size();
    vector<int> next = getNext(pattern);

    int i = 0, j = 0;
    while (i < n && j < m)
    {
        if (str[i] == pattern[j])
        {
            i++;
            j++;
        }
        else if (j == 0)
        {//无法回退 i + 1
            i++;
        }
        else//j回退 i不回退
            j = next[j - 1];
    }

    if (j == m) //匹配成功,返回索引
        return i - m;
    else//匹配失败
        return -1;
}

 测试代码

int main()
{
    string s = "abacadabrabracabracadabrabrabracad";
    string p1 = "rab";
    string p2 = "abracadabra";
    string p3 = "abacad";


    cout << "第一个子串的索引为:" << KMP(s, p1) << endl; //8
    cout << "第二个子串的索引为:" << KMP(s, p2) << endl; //14
    cout << "第三个子串的索引为:" << KMP(s, p3) << endl; //0
    return 0;
}

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值