MENU

kmp算法的个人看法

September 18, 2020 • Read: 177 • c/c++基本算法

kmp算法的个人看法(未完……)

请输入图片描述

一、引入问题(例题解析)

这道题估计用kmp算法比较方便一些,况且kmp也是数据结构应该必学的和考研必考的,看了这个算法好几天才弄明白,真是菜到家了。先把这道题的代码安排一下。

/*
 * @Author       : 剑枫
 * @Date         : 2020-09-12 16:20:33
 * @LastEditors  : 剑枫
 * @LastEditTime : 2020-09-16 18:18:56
 * @FilePath     : \博客园\文章博客\195.cpp
 */
#include <bits/stdc++.h>
using namespace std;
/* P 为模式串,下标从 0 开始 */
void GetNext(string P, int next[])
{
    int p_len = P.size();
    int i = 0; // P 的下标
    int j = -1;
    next[0] = -1;
    while (i < p_len)
    {
        if (j == -1 || P[i] == P[j])
        {
            i++;
            j++;
            next[i] = j;
        }
        else
            j = next[j];
    }
}
/* 在 S 中找到 P 第⼀次出现的位置 */
int KMP(string S, string P, int next[])
{
    GetNext(P, next);
    int i = 0; // S 的下标
    int j = 0; // P 的下标
    int s_len = S.size();
    int p_len = P.size();
    while (i < s_len && j < p_len) // 因为末尾 '\0' 的存在,所以不会越界
    {
        if (j == -1 || S[i] == P[j]) // P 的第⼀个字符不匹配或 S[i] == P[j]
        {
            i++;
            j++;
        }
        else
            j = next[j]; // 当前字符匹配失败,进⾏跳转
    }
    if (j == p_len) // 匹配成功
        return i - j;
    return -1;
}
int main()
{
    int next[1002], g, n;
    char a[1009], b[1080];
    while (cin>>n)
    {
        memset(next, 0, sizeof(next));
        
        for (int i = 0; i < n; i++)
        {
            scanf("%s %s",a,b);
            g = KMP(a, b, next);
            if (g != -1)
                printf("Case #%d: Yes\n",i+1); // 15
            else
            {
                printf("Case #%d: No\n",i+1);
            }
        }
    }

    return 0;
}

二、为啥来的

我们可能会遇到一些题目,比如,让你在一个主串中查找一个给定的字符串,或者让你查找一个字符串在主串中出现的位置。按照我们以往的思想就是纯暴力解决,

/* 字符串下标始于 0 ,S是主串,P是模式串(也就是要寻找的那个字符串)*/
int NaiveStringSearch(string S, string P)
{
    int i = 0; // S 的下标
    int j = 0; // P 的下标
    int s_len = S.size();
    int p_len = P.size();
    while (i < s_len && j < p_len)
    {
        if (S[i] == P[j]) // 若相等,都前进⼀步
        {
            i++;
            j++;
        }
        else // 不相等
        {
            i = i - j + 1;
            j = 0;
        }
    }
    if (j == p_len) // 匹配成功
        return i - j;
    return -1;
}

但是我们会发现这个暴力解决的方法的时间复杂度是O(mn),

然后我们为了更快算出,就引用了kmp算法。

三,kmp初级介绍(未优化)

1、在这里强烈建议您看一下三哥的kmp算法介绍视频请输入链接描述

2、(1)https://segmentfault.com/img/remote/1460000021457884

首先,主串"BBC ABCDAB ABCDABCDABDE"的第一个字符与模式串"ABCDABD"的第
一个字符,进行比较。因为 B 与 A 不匹配,所以模式串后移一位。

(2)https://segmentfault.com/img/remote/1460000021457876

因为 B 与 A 又不匹配,模式串再往后移。
(3)请输入图片描述

就这样,直到主串有一个字符,与模式串的第一个字符相同为止。

(4)请输入图片描述
接着比较主串和模式串的下一个字符,还是相同。

(5)请输入图片描述
直到主串有一个字符,与模式串对应的字符不相同为止。

(6)https://segmentfault.com/img/remote/1460000021457882

这时,最自然的反应是,将模式串整个后移一位,再从头逐个比较。这样做虽然可行,
但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

(7)请输入图片描述

一个基本事实是,当空格与 D 不匹配时,你其实是已经知道前面六个字符
是"ABCDAB"。KMP 算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已
经比较过的位置,而是继续把它向后移,这样就提高了效率

怎么做到这一点呢?可以针对模式串,设置一个跳转数组 int next[] ,这个数组是怎么计
算出来的,后面再介绍,这里只要会用就可以了

(8)请输入图片描述
已知空格与 D 不匹配时,前面六个字符"ABCDAB"是匹配的。根据跳转数组可知,不匹
配处 D 的 next 值为 2,因此接下来 从模式串下标为 2 的位置开始匹配。

(9)请输入图片描述
因为空格与 C 不匹配,C 处的 next 值为 0,因此接下来模式串从下标为 0 处开始匹配。

(10)请输入图片描述

因为空格与 A 不匹配,此处 next 值为 -1,表示模式串的第一个字符就不匹配,那么直接
往后移一位。

(11)请输入图片描述

逐位比较,直到发现 C 与 D 不匹配。于是,下一步从下标为 2 的地方开始匹配。

(12)请输入图片描述

逐位比较,直到模式串的最后一位,发现完全匹配,于是搜索完成。

四、next数组的由来

例如字符串“ababcdabcd” 我们将首字符,next【0】=-1;

i01234567
模式串abaabca'0'
next[i]-10011201

i=0,我们一般将模式串的首字符,计做next【0】=-1;

i=1,前面字符串为‘a’,其最长相同真前后缀长度为0,即next[1]=0;

i=2,前面字符串为‘ab’,其最长相同真前后缀长度为0,即next[2]=0;

i=3,前面字符串为‘aba’,其最长相同真前后缀长度为a,即next[3]=1;

i=4,前面字符串为‘abaa’,其最长相同真前后缀长度为1,即next[4]=1;

i=5,前面字符串为‘abaab’,其最长相同真前后缀长度为2,即next[5]=2;

i=6,前面字符串为‘abaabc’,其最长相同真前后缀长度为0,即next[6]=0;

i=7,前面字符串为‘abaabca’,其最长相同真前后缀长度为1,即next[7]=1;

那么,为什么根据最长相同真前后缀的长度就可以实现在不匹配情况下的跳转呢?举个
代表性的例子:假如 i = 5时不匹配,此时我们是知道其位置前的字符串为 abaab,
仔细观察这个字符串,首尾都有一个ab ,既然在 i = 5 处的 c 不匹配,我们为何不直
接把 i = 2 处的 c拿过来继续比较呢,因为都有一个 ab 啊,而这个 ab 就
是 abaab 的最长相同真前后缀,其长度 2 正好是跳转的下标位置。
有的读者可能存在疑问,若在 i = 4 时匹配失败,按照我讲解的思路,此时应该把 i =
1 处的字符拿过来继续比较,但是这两个位置的字符是一样的啊,都是 b ,既然一样,
拿过来比较不就是无用功了么?
所以我们接下来会讲这个算法的优化,您先别着急。

/* P 为模式串,下标从 0 开始 */
void GetNext(string P, int next[])
{
    int p_len = P.size();
    int i = 0; // P 的下标
    int j = -1;
    next[0] = -1;
    while (i < p_len)
    {
        if (j == -1 || P[i] == P[j])
        {
            i++;
            j++;
            next[i] = j;
        }
        else
            j = next[j];
    }
}

五、优化kmp算法

在这里我们就会解决上面抛出的问题,如果遇到下面问题

i01234567
模式串abcdabd'0'
next[i]-10000120

若在 i = 5 时匹配失败,按照未优化的代码,此时应
该把 i = 1 处的字符拿过来继续比较,但是这两个位置的字符是一样的,都是 b ,既然
一样,拿过来比较就是无用功了。之所以会这样是因为
KMP 还未优化。那怎么改写就可以解决这个问题呢?很简单。

//P 为模式串,下标从 0 开始 
    void GetNextval(string P, int nextval[])
{
    int p_len = P.size();
    int i = 0; // P 的下标
    int j = -1;
    nextval[0] = -1;
    while (i < p_len)
    {
        if (j == -1 || P[i] == P[j])
        {
            i++;
            j++;
            if (P[i] != P[j])
                nextval[i] = j;
            else
                nextval[i] = nextval[j]; // 既然相同就继续往前找真前缀
        }
        else
            j = nextval[j];
    }
}

本文参考:kmp经典算法