欢迎光临
我们一直在努力

数据结构 —— KMP算法详解(配图解)

目录

一、 为什么朴素算法效率慢?——“遗忘”带来的代价

1. “回溯”机制导致的重复劳动

2.图解和代码演示

3.极端案例演示

二、 KMP算法如何高效解决问题?——“记忆”与“滑动”

1. 挖掘模式串的“自相似性”

2. Next数组的创建

3.KMP具体优化的过程和代码(图解)

4.KMP算法解决极端案例

5. 复杂度质变的主要原因

三、算法的差异总结

一、 为什么朴素算法效率慢?——“遗忘”带来的代价

 朴素匹配算法之所以在最坏情况下的时间复杂度高达 O(n×m)O(n×m) 。最主要的原因就是它完全丢弃了已经匹配过的信息。

1. “回溯”机制导致的重复劳动

在朴素算法中,当主串的第 i 个字符与模式串的第 j 个字符失配时,算法会做两件事:

1.主串指针回退:回到上一次开始匹配位置的下一个字符(即 i−j+1i−j+1 )。

2.模式串指针重置:回到起点(即 00 )。        

 问题的所在: 主串指针的回退意味着我们重新扫描了那些已经被验证过的字符。在很多场景下(特别是模式串包含大量重复前缀时),这些被回退扫描的区域根本不可能产生新的匹配,但朴素算法依然机械地进行了比较。

    2.图解和代码演示

    下面的图解是简单的字符匹配,图解后面给出了在“极端情况”进行匹配出现的一些问题.

    朴素算法代码实现

    int strMatch(char* str, char* pattern)
    {
    //获取字符串长度
    int m = strlen(str);
    int n = strlen(pattern);
    //主要串指针
    int i = 0;
    for (i = 0; i <= m – n; i++)
    {
    //模串指针
    int j = 0;
    while (j < n)
    {
    //只有相等时i和j才++
    if (str[i] == pattern[j])
    {
    i++;
    j++;
    }
    else
    {
    i = i – j;
    break;

    }
    }
    //这里说明匹配成功了,范围下标的位置.
    if (j == n)
    {
    return i – j;
    }
    }
    return -1;
    }

    3.极端案例演示

    如果是图片中的字符串显然是可以匹配成功,但假设主串 S="AAAAAA…B" 摸串 P="AAAAB" 呢?

    • 第一次匹配:前4个 'A' 都匹配,第5个字符 'B' vs 'A' 失败。朴素算法回退主串指针。
    • 第二次匹配:从主串第2个 'A' 开始,又匹配了3个 'A',再次失败。
    • …
    • 每一次失配,主串指针都要回头走很远,导致大量的无效比较。这就是所谓的 “由于缺乏记忆而导致的指数级爆炸”。
    • 显然这种算法是非常低效的。所以就有了三位大哥发明了KMP算法,这个算法就可以很好的去解决这个问题。

    二、 KMP算法如何高效解决问题?——“记忆”与“滑动”

    KMP算法的核心是:主串指针绝不回头。为了实现这一点,它必须解决一个问题:“当失配发生时,主串不动,那么模式串要何去何从呢?”

    1. 挖掘模式串的“自相似性”

    KMP发现,失配点之前的子串往往蕴含着规律。如果模式串的前缀和后缀有重叠部分(例如 "ABCDAB" 中,前缀 "AB" 和后缀 "AB" 相同),那么当后缀部分匹配成功但随后失配时,我们可以直接把前缀“滑”到后缀的位置继续比对。

    这种“最长相等前后缀”的信息,就是KMP的记忆。

    2. Next数组的创建

    next数组的创建:基于模式串进行创建的next数组

    用途:利用next数组来完成字符串的匹配。当失配时,用来确定下一次所要匹配的位置。

    例如:模式串 S = “abdab”。创建方法图所示:

    3.KMP具体优化的过程和代码(图解)

    KMP算法代码实现

    //kmp算法实现
    //查找在文本中是否出现过此字符串,返回早出现的位置。
    void getNext(char* pattern, int* next)
    {
    //获取模式串
    int m = strlen(pattern);
    int i = 0;//后缀指针(当前正在计算的字符位置)
    int j = -1;//前缀指针(当前最长相等前后缀的长度索引)
    next[0] = -1;//初始化:第一个字符没有前缀
    while (i < m)
    {
    //情况1:j回退到了起点,或者当前字符匹配成功
    if (j == -1 || pattern[i] == pattern[j])
    {
    i++;
    j++;
    next[i] = j;//记录当前位置的最长匹配长度
    }
    //情况2:字符不匹配,利用已有的 next 数组进行"回退"
    else
    {
    j = next[j];
    }
    }
    }
    int kmp(char* str, char* pattern)
    {
    //获取字符串个数
    int n = strlen(str);
    int m = strlen(pattern);

    //动态内从开辟 next 数组空间
    int* next = (int*)malloc((m + 1) * sizeof(int));
    if (next == NULL)
    {
    perror("kmp:next");
    return 0;
    }

    //计算模式串的 next 数组
    getNext(pattern, next);

    //主串指针
    int i = 0;
    //模式串指针
    int j = 0;

    //开始匹配循环
    while (i < n && j < m)
    {
    //情况1:j == -1模式串第一个字符就匹配失败,或者当前字符匹配成功
    if (j == -1 || str[i] == pattern[j])
    {
    i++;//主串指针后移
    j++;//模式串指针后移
    }
    //情况2:当前字符失配
    else
    {
    //模式串指针后退,主串指针 i 不动
    j = next[j];

    }

    }
    //匹配成功
    if (j == m)
    {
    //返回起始下标.
    return i – j;
    }
    //匹配失败
    else
    {
    return -1;
    }
    //释放空间
    free(next);
    next = NULL;
    }

    4.KMP算法解决极端案例

    如果回到上面的极端案例效率是否会提高呢?极端案例:主串 S="AAAAAA…B",摸串P="AAAAB"

    朴素算法:失配后,主串指针回退,模式串归零。

    KMP算法:

  • 当在第5位失配时,KMP查看 next 数组。
  • 发现前4个字符 "AAAA" 的最长相等前后缀长度是 3(即 "AAA")。
  • 动作:主串指针不动!模式串指针直接跳到索引 3 的位置(也就是第4个 'A')。
  • 结果:直接利用已知的 "AAA" 重叠部分,跳过了中间无意义的比较步骤。
  • 5. 复杂度质变的主要原因

    主串单向扫描:主串指针 i 在整个过程中只增不减,最多移动 n 次。

    模式串的高效跳跃:虽然模式串指针 j 会回退,但它是根据 next 数组进行“大幅度跳跃”,而不是盲目归零。数学上可以证明,模式串指针回退的总次数也是线性的。

    结论:两者相加,总操作次数被严格控制在 O(n+m)O(n+m) 级别。

    三、算法的差异总结

    特性朴素算法 (BF)KMP算法
    对待历史的态度 健忘:一旦失配,抛弃所有已匹配信息,从头再来。 铭记:利用已匹配部分的内部结构(前后缀),保留有效信息。
    指针行为 双指针回溯(主串回退,模式串重置)。 单指针前进(主串不回退,模式串智能滑动)。
    核心瓶颈 在重复字符多的文本中,存在大量冗余比较。 消除了所有冗余比较,每个字符至多被有效比较常数次。
    本质区别 暴力穷举搜索。 基于状态机的确定性转移。
    赞(0)
    未经允许不得转载:171主机测评 » 数据结构 —— KMP算法详解(配图解)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址