【算法详解】理解KMP,真的那么难吗?—— 一篇讲透它的核心思想

【算法详解】理解KMP,真的那么难吗?—— 一篇讲透它的核心思想
在这里插入图片描述



🫧 励志不掉头发的内向程序员个人主页
 ✨️ 个人专栏: 《C++语言》《Linux学习》

🌅偶尔悲伤,偶尔被幸福所完善


👓️博主简介:

在这里插入图片描述


文章目录


前言

本文用尽量详细的语言来讲解说明 kmp 算法内容,学习之前需要知道一点点动态规划的基础,如果不知道最好去了解了解。我们一起来看看算法吧。
在这里插入图片描述

一、相关概念

在学习 kmp 算法之前,我们得先提前了解最基本的 “ 动态规划 ” 的知识,否则可能学习的时候会有一些困难,因为它的原理类似于动态规划。

字符串:

  • 用字符构成的的序列就是字符串。

这个概念很简单,但是我们这里有个小技巧:就和动态规划那样,在字符串匹配问题中,我们会让字符串的下标从 1 开始,这样便于我们处理一些边界问题。因此,在输入字符串时,我们一般会在前面加上一个空格,这样字符串就会从 1 开始计数了。

string s; cin >> s;int n = s.size(); s =' '+ s;

子串:

  • 选取字符串中连续的一段。

例如:

字符串: abcdefg;
子串:cdefg、cde、abcdefg、a、b 等等;

前缀:

  • 从字符串首端开始,到某一个位置结束的子串。

字符串长度为 i 的前缀,就是字符串 [ 1, i ] 区间的子串。

例如:

字符串:abcdefg;
长度为 3 的前缀为:abc;
长度为 5 的前缀为:abcde;
长度为 7 的前缀为:abcdefg(字符串本身)

注意:
这里默认字符串前面有一个空格,所以下标从 1 开始而非 0。

真前缀:

  • 不包含字符串本身的前缀。

后缀:

  • 从字符串某一个位置开始,到字符串末端的结束的子串。

字符串长度为 i 的后缀,就是字符串 [ n − i + 1, n ] 区间的子串。

例如:

字符串:abcdefg;
长度为 3 的后缀为:efg;
长度为 5 的后缀为:cdefg;
长度为 7 的后缀为:abcdefg(字符串本身)

真后缀:

  • 不包含字符串本身的后缀。

真公共前后缀 / border 以及最长真公共前后缀的长度 / π:

  • 字符串的真公共前后缀为⼀个子串 ,满足既是真前缀,又是真后缀,也称为字符串的 border。
  • 在一个字符串中,最长的真公共前后缀的长度用 π 表示。

例如:

字符串:aabaaba;
真前缀:a、aa、aab、aaba、aabaa、aabaab、aabaaba;
真后缀:a、ba、aba、aaba、baaba、abaaba、aabaaba;

从上面我们可以发现,真前缀和真后缀相同的有 a、aaba。最长真公共前后缀的长度为 4。所以上述字符串的 π 值为 4 。

  • 性质:

传递性:字符串 s 的 border 的 border 也是字符串 s 的 border。

在这里插入图片描述

字符串匹配:

  • 字符串匹配又称模式匹配。给定两个字符串 S 和 T ,需要在主串 S 中找到模式串 T 。

例如:

主串 S = “abcdefcde”
模式串 T = “cde”
如果下标从 1 开始计数,模式串会在主串 3、7 位置出现。

关于字符串匹配,大家肯定能想到暴力解法,那就是拿着模式串,在主串中一个位置一个位置判断。但是,暴力解法极限情况,时间开销会达到 O(n × m)。而接下来要学习的 kmp,能在 O(n + m) 的线性时间内,找到所有的匹配位置,以及维护出更多的信息。

二、前缀函数

  • 字符串每一个前缀子串的 π 值。

以字符串 aabaab 为例,π[i] 表示:字符串 s 长度为 i 的前缀,最长的 border 长度(最长真公共前后缀)。

下标(i)123456
前缀子串aaaaabaabaaabaaaabaab
π010123

原理:

在这里插入图片描述

技巧:

  • 从大到小枚举字符串 s 某个前缀的所有 border。

假设我们此时生成了一个字符串 s 的前缀函数表,我们可以利用这张表,从大到小拿到某个前缀所有的 border。

原理就是 border 的传递性:字符串 border 的 border 还是 border。

在这里插入图片描述


到最后如果是没有 π 时,我们函数就会跳到我们提前给字符串预留的边界情况发生的 0 下标位置上。

string s;// 生成好的前缀函数int pi[N];// 长度为 i 的前缀中,所有 border 的长度voidget_border(int i){int j = pi[i];while(j){ cout << j; j = pi[j];}}

三、计算前缀函数

我们在看完上面的内容后我们就来看看前缀函数怎么计算。

计算前缀函数:

  • 计算前缀函数包含动态规划的思想,那就是推导状态转移方程。

对于字符串 s:

  1. 状态表示:
    π[i] 表示:字符串 s 长度为 i 的前缀,最长的 border 长度(最长真公共前后缀)。

在计算完 i - 1 位置的前缀函数后,此时开始计算 i 位置的 π 值。

在这里插入图片描述

我们可以发现,我们 i 位置前上一个位置的真前缀和和真后缀和都存在 π[i - 1] 中。换句话说,如果 i 位置元素和 j 位置的元素相同,那就是 i 位置的 π 值了。

在这里插入图片描述


如果 i 和 j 不相同,我们可以继续往上一层的 π 值去寻找,也就是 π[ π [ i - 1 ] ]。

在这里插入图片描述


此时如果 s[ j -1 ] == s[ i ] ,此时便得出了 i 位置的 π 值了,否则我们在往下推。

  1. 状态转移方程:
  • 我们发现,如果将长度为 i 的前缀中的 border 删去最后一个字符,就变成了长度为 i - 1 的前缀中的 border;
  • 那么,我们就可以从大到小枚举长度为 i - 1 的前缀中所有的 border,然后判断这个 border 的下⼀个字符是否和 s[i] 相等:
    a. 如果相等,说明这个就是最长的;
    b. 如果不相等,那就继续判断下⼀个 border,直到将所有的 border 验证完毕。
string s;int pi[N];voidget_pi(int i){ cin >> s;int n = s.size(); s =' '+ s;for(int i =2; i <= n; i++){int j = pi[i -1];while(j && s[i]!= s[j +1]) j = pi[j];if(s[i]== s[j +1]) j++; pi[i]= j;}}

当然,我们还可以进行优化,我们可以发现,当我们找到 i - 1 的时候,此时的 j 刚好就是在我们下一次要查找的 π 值。

string s;int pi[N];voidget_pi(int i){ cin >> s;int n = s.size(); s =' '+ s;for(int i =2, j =0; i <= n; i++){while(j && s[i]!= s[j +1]) j = pi[j];if(s[i]== s[j +1]) j++; pi[i]= j;}}

时间复杂度:
模拟一遍过程会发现,指针每次向后移动一位后,指针最多会向后移动一位,然后继续往前跳。因此两个指针最差情况下会遍历字符串两遍,时间复杂度为 O(2n) = O(n)。

四、用前缀函数解决字符串匹配

设主串 S = “abcabaaaba”,模式串 T = “aba”,主串的长度为 n,模式串的长度为 m。

将两个字符串拼起来:S = T + ‘#’ + S = “aba#abcabaaaba”,对于新的字符串,可以在 O(n + m) 时间内生成前缀函数:

下标1234567891011121314
π00101201231123

前缀函数等于模式串长度的位置 i ,就是能够匹配的末端。在主串中,出现的位置就是 i − 2 × m 。那么,有了前缀函数之后,不仅能知道匹配了几次,还能知道每次匹配的起始位置。

注意:
当 i > m + 1 时,如果出现了 π == m 时,那就是出现了匹配的情况,匹配出现的位置为 i - 2 × m。例如上面 m == 3,π 为 3 都是匹配的,比如 i ==10、i == 14 的位置都能匹配上,它们分别对应主串的 4(10 - 2 × 3) 和 8(14 - 2 × 3)位置。

五、kmp 算法模板

题目来源: 洛谷

题目链接:P3375 【模板】KMP - 洛谷

【题目描述】

【输入格式】

【输出格式】

【示例】

我们按照上面的实现方式即可完成此题。

#include<iostream>#include<string>usingnamespace std;constint N =2e6+10; string s, t;int n, m;int pi[N];intmain(){ cin >> s >> t; n = s.size(); m = t.size(); s =' '+ t +'#'+ s;for(int i =2; i <= n + m +1; i++){int j = pi[i -1];while(j && s[j +1]!= s[i]) j = pi[j];if(s[j +1]== s[i]) j++; pi[i]= j;if(j == m){ cout << i -2* m << endl;}}for(int i =1; i <= m; i++) cout << pi[i]<<" "; cout << endl;return0;}

六、next 数组版本

next 数组版本其实和上面是一样的,只不过是把上面的过程拆成两步来写:

  1. 先预处理模式串 t 的前缀函数 - next 数组;
  2. 在暴力匹配的过程中,用生成的 next 数组,加速匹配。

next 数组的本质也是求解我们的前缀函数。我们来举一个例子:

在这里插入图片描述


我们先把 t 的 next 数组求出来。

在这里插入图片描述


此时我们拿 t 来和 s 进行匹配。

在这里插入图片描述


当我们发现匹配不上的时候,如果是暴力匹配,那 t 就得从头开始继续往后匹配,但是由于我们求了 t 的前缀数组,所以我们可以利用数组来加速匹配。

在这里插入图片描述
#include<iostream>#include<string>usingnamespace std;constint N =2e6+10; string s, t;int n, m;int ne[N];intmain(){ cin >> s >> t; n = s.size(); m = t.size(); s =' '+ s; t =' '+ t;// 预处理模式串的 next 数组for(int i =2, j =0; i <= m; i++){while(j && t[i]!= t[j +1]) j = ne[j];if(t[i]== t[j +1]) j++; ne[i]= j;}// 利⽤ next 数组匹配for(int i =1, j =0; i <= n; i++){while(j && s[i]!= t[j +1]) j = ne[j];if(s[i]== t[j +1]) j++;if(j == m){ cout << i - m +1<< endl;}}for(int i =1; i <= m; i++) cout << ne[i]<<" "; cout << endl;return0;}

七、周期和循环节


总结

以上便是 kmp 算法的原理啦,还是有一点点绕的,大家可以多琢磨琢磨,代码不是很难写,但是理解不容易,希望大家多加思考,我们下一章节再见。

🎇坚持到这里已经很厉害啦,辛苦啦🎇ʕ • ᴥ • ʔづ♡ど

Read more

政安晨【零基础玩转开源AI项目】OpenClaw 跨平台AI助手完全使用指南:从入门到精通 (基于我这段时间在Ubuntu Linux系统上的使用经验为大家总结一下)

政安晨【零基础玩转开源AI项目】OpenClaw 跨平台AI助手完全使用指南:从入门到精通 (基于我这段时间在Ubuntu Linux系统上的使用经验为大家总结一下)

政安晨的个人主页:政安晨 欢迎 👍点赞✍评论⭐收藏 希望政安晨的博客能够对您有所裨益,如有不足之处,欢迎在评论区提出指正! 【详细安装过程见我博客的上上篇文章】 目录 第一章:OpenClaw核心概念与架构 1.1 什么是OpenClaw? 1.2 OpenClaw技术架构 1.3 支持的模型 第二章:安装与配置 2.1 系统要求 2.2 快速安装(推荐) 2.3 从源码安装(开发版) 2.4 Docker安装 2.5 配置文件详解 第三章:通道配置详解 3.1 飞书配置 3.2 Telegram配置 3.

By Ne0inhk
Qwen3.5开源矩阵震撼发布!从0.8B到397B,不同规模模型性能、显存、速度深度对比与选型指南来了!

Qwen3.5开源矩阵震撼发布!从0.8B到397B,不同规模模型性能、显存、速度深度对比与选型指南来了!

截至今天2026年3月3日,Qwen3.5已形成从0.8B到397B的完整开源矩阵,分为轻量稠密(0.8B/2B/4B/9B/27B)、中型MoE(35B-A3B/122B-A10B)、旗舰MoE(397B-A17B)三大梯队。不同尺度在性能、显存、速度、场景上差异显著,下面是完整对比与选型指南,仅供参考。 一、Qwen3.5全尺度核心参数总览(2026.3最新) 1.轻量稠密系列(Dense,个人/边缘/轻量服务) 名称总参数激活参数架构上下文显存****FP164bit****量化显存定位Qwen3.5-0.8B0.8B0.8BDense32K1.6GB0.4GB极致轻量、端侧/实时交互Qwen3.5-2B2B2BDense32K4GB1GB移动端/IoT、低延迟对话Qwen3.5-4B4B4BDense64K8GB2GB轻量Agent、多模态基座Qwen3.

By Ne0inhk
终于有人把Openclaw团队协作版讲明白了!Clawith 开源方案从原理到部署全拆解

终于有人把Openclaw团队协作版讲明白了!Clawith 开源方案从原理到部署全拆解

Clawith 深度拆解:如何用开源方案搭建多 Agent 团队协作平台 快速摘要 Clawith 是一个基于 OpenClaw 生态的开源多智能体协作平台,它解决了 OpenClaw 在团队场景下「Agent 之间互不认识、缺乏组织架构、没有权限管控」的三大核心痛点。 通过引入 Aware 自主感知系统、数字员工身份体系和广场知识沉淀机制,Clawith 让多个 AI Agent 具备了真正的团队协作能力。项目采用 Apache 2.0 开源协议,支持 Docker 一键部署,最低 2 核 CPU + 4GB 内存即可运行。往下看,有从底层原理到实际部署的完整拆解。 一、从 OpenClaw 到 Clawith:为什么需要「团队版」

By Ne0inhk
【机器人】复现 RoboBrain2.0 具身大脑模型 | 统一感知、推理和规划能力

【机器人】复现 RoboBrain2.0 具身大脑模型 | 统一感知、推理和规划能力

RoboBrain 2.0是一个机器人的具身大脑模型,具备统一感知、推理和规划能力; 同时适应对物理环境中复杂的具身任务; 它提供不同版本:轻量级的3B、7B模型和全尺寸的 32B 模型,包含视觉编码器和语言模型。 代码地址:https://github.com/FlagOpen/RoboBrain2.0 论文地址:RoboBrain 2.0 Technical Report 目录 快速了解模型 1、创建Conda环境 2、安装依赖库 3、安装torch 4、模型推理 示例1:图文问答,使用RoboBrain2.0-7B模型,不开思考模式 示例2:图文问答,使用RoboBrain2.0-7B模型,开启思考模式 示例3:图文问答,使用RoboBrain2.0-3B模型 示例4:

By Ne0inhk