跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

KMP 算法原理与 C++ 实现详解

KMP 算法是一种高效的字符串匹配算法,通过预处理模式串生成 next 数组,避免主串指针回溯。核心在于最长公共前后缀概念,利用已匹配信息优化回退逻辑。时间复杂度 O(n+m),空间复杂度 O(m)。适用于文本查找、编译器词法分析及生物序列匹配等场景。

嘘发布于 2026/3/28更新于 2026/5/2326 浏览
KMP 算法原理与 C++ 实现详解

前言

KMP 算法是由 Knuth、Morris、Pratt 三位学者共同提出的字符串匹配高效算法,核心解决了传统暴力匹配算法中'主串指针回溯'的问题,将时间复杂度优化到线性级别。

一、算法概述

1. 解决的问题

字符串匹配问题:给定主串 S 和模式串 P,判断模式串 P 是否是主串 S 的子串,若存在,返回其在主串中首次出现的起始索引;若不存在,返回 -1。

2. 传统暴力匹配的问题

暴力匹配(BF 算法)的思路是:主串指针 i 和模式串指针 j 从起始位置开始逐一匹配,若 S[i] != P[j],则 i 回溯到 i-j+1,j 重置为 0,重新匹配。

  • 问题核心:主串指针的回溯导致大量重复比较,最坏时间复杂度为 O(n*m)(n 为主串长度,m 为模式串长度)。
  • 例子:主串 S = 'AAAAAAB',模式串 P = 'AAAB',暴力匹配会在第 4 位匹配失败后反复回溯,效率极低。

动态演示

暴力匹配算法动态演示图

3. KMP 算法的核心改进

KMP 算法通过预处理模式串 P,生成 next 数组,让主串指针 i 永不回溯,仅通过模式串指针 j 的回退实现匹配,将时间复杂度优化到 O(n+m)。

  • 核心思想:当 S[i] != P[j] 时,利用已匹配的前缀信息,让 j 回退到当前位置的最长公共前后缀的后缀末尾下一位,而非直接重置为 0,避免重复比较。

二、算法核心原理

理解 KMP 的关键,先掌握最长公共前后缀这个核心概念,再理解 next 数组的作用。

关键概念:最长公共前后缀(真前后缀)

对于模式串的子串 P[0…j],真前缀是指不包含最后一个字符的所有前缀,真后缀是指不包含第一个字符的所有后缀;最长公共前后缀就是真前缀和真后缀中长度最大的相等子串。

  • 注意:真前后缀不能是子串本身,否则无意义
  • 若子串无相等的真前后缀,最长公共前后缀长度为 0(如 P[0…1]=AB)。

实例:字符串 P = 'abcab',前缀有'a','ab','abc','abca', 后缀有'b','ab','cab','bcab'。最长公共前后缀字符串就是'ab',长度为 2。

在这里插入图片描述

对于字符串"aaaaa",求最长公共前后缀长度的过程如下:

在这里插入图片描述

对于字符串"abacb",求最长公共前后缀长度的过程如下:

在这里插入图片描述

三、next 数组计算详解(KMP 的灵魂)

next 数组是 KMP 的核心,next 数组的长度等于模式串长度 m,next[j] 表示模式串子串 P[0…j] 的最长公共前后缀的长度。

1. 为什么需要 next?

当模式串跟主串匹配失败时,不用回退主串指针 i,只需要回退模式串的指针 j。而模式串移动多少?靠的就是 next 数组。

直觉理解: 当模式串跟主串已经匹配到 i,却在 i+1 失败时;前面已经匹配的那一段里,有一部分前缀 = 后缀,这部分前缀就可以'复用',不用再重新匹配了。

  • 作用:当 S[i] != P[j+1] 时,模式串 j 需要回退到 next[j] 的位置,继续匹配;
  • 本质:next 数组是模式串的自我匹配,通过预处理模式串,记录每个位置的最长公共前后缀长度,避免匹配失败时的重复比较。

图解

KMP 根据 next 第一次回退

KMP 根据 next 第二次回退

KMP 匹配成功

2. next 的暴力方法求解

遍历模式串 p 的每一个子串,再对每个子串遍历所有长度的前后缀,这就是暴力求解 next 数组的方法(时间复杂度:O(m²)(m 为模式串长度))

// 暴力法计算 next 数组
std::vector<int> getNextBruteforce(const std::string& p){
    int m = p.size();
    std::vector<int> next(m, 0);
    for(int i = 1; i < m; i++){
        for(int k = i; k >= 1; k--){
            if(p.substr(0, k) == p.substr(i - k + 1, k)){
                next[i] = k;
                break;
            }
        }
    }
    return next;
}

3. next 数组的计算思路(迭代法)

初始化两个指针:i 为后缀指针(从 1 开始,遍历模式串),j 为前缀指针(从 0 开始,记录最长公共前后缀的长度),next[0] = 0(单个字符无真前后缀)。

遍历模式串 P[1…m-1],分两种情况更新 i 和 j,并赋值 next[i]:

  • 情况 1: P[i] == P[j] 说明当前字符匹配,最长公共前后缀长度 + 1,执行:j++,next[i] = j,i++。
  • 情况 2: P[i] != P[j] 说明当前字符不匹配,需要回退前缀指针 j,利用已计算的 next 数组信息,执行:j = next[j-1](直到 j == 0 或 P[i] == P[j])。
    • 若回退后 j == 0 且 P[i] != P[j]:next[i] = 0,i++;
    • 若回退后 P[i] == P[j]:执行情况 1 的逻辑。

4. 迭代法的正确性分析

(1). 动态规划的思路 计算 next[i] 时,我们已经知道 next[0], next[1], …, next[i-1]。我们利用已计算的 next 值来高效计算 next[i]。

(2). 算法的直观解释

步骤 1:基础情况(i=1)—— 验证初始步骤正确 当 i=1 时,处理子串 P[0…1]:

  • 初始化 j=next[0]=0(P[0] 的最长真前后缀长度为 0,符合定义);
  • 若 P[1] == P[0](如模式串 AA)
    • 执行情况 1:j++=1,next[1]=1——P[0…1] 的最长相等真前后缀长度为 1(P[0]=P[1]),符合定义;
  • 若 P[1] != P[0](如模式串 AB):
    • 执行情况 2:j=0 且 P[1] != P[0],next[1]=0 —— P[0…1] 无相等真前后缀,符合定义。

结论:基础情况(i=1)的 next 值计算完全符合定义,正确。

步骤 2:归纳假设 假设对于所有 0 ≤ k < i,next[k] 的计算都是正确的 —— 即 next[k] 是 P[0…k] 的最长相等真前后缀长度。

步骤 3:归纳推导(Inductive Step)—— 证明 next[i] 正确 基于归纳假设,分析 P[0…i] 的最长相等真前后缀长度(即 next[i]),分两种核心情况:

情况 A:如果 P[i] == P[j](j=next[i-1])

  • 由归纳假设,j = next[i-1] 是 P[0…i-1] 的最长相等真前后缀长度 → 前缀 P[0…j-1] = 后缀 P[i-j…i-1];
  • 现在 P[i] == P[j],相当于给这两个相等的前后缀'末尾各加一个相同字符' → 前缀 P[0…j] = 后缀 P[i-j…i];
  • 这个新的长度 j+1 是 P[0…i] 的最长相等真前后缀长度(因为 j 已经是 P[0…i-1] 的最长长度,加 1 后自然是当前最长);
  • 迭代法执行 j++、next[i]=j,完全匹配这个最长长度,因此 next[i] 正确。

实例验证:模式串 AAAB,i=2(P[2]=A),j=next[1]=1(P[0…1] 的最长长度 1):

  • P[2]=A == P[1]=A → j++=2,next[2]=2;
  • P[0…2](AAA)的最长相等真前后缀长度为 2(AA ↔ AA),符合定义。

情况 B:如果 P[i] != P[j](j=next[i-1]) 此时 P[0…i-1] 的最长前后缀加 P[i] 后不匹配,需要找 P[0…i-1] 的次长相等真前后缀,直到找到匹配或 j=0:

  • 为什么回退 j = next[j-1]? 由归纳假设,next[j-1] 是 P[0…j-1] 的最长相等真前后缀长度 → 这意味着: ① P[0…next[j-1]-1] = P[j-next[j-1]…j-1](P[0…j-1] 的最长相等前后缀); ② 结合 P[0…j-1] = P[i-j…i-1](j=next[i-1] 的定义),可推导出 P[0…next[j-1]-1] = P[i-next[j-1]…i-1]; ③ 因此 next[j-1] 是 P[0…i-1] 的次长相等真前后缀长度(最优回退,无需从头找)。
  • 回退后的两种子情况: ① 回退后 P[i] == P[j]:执行场景 1 的逻辑,next[i] = j+1(次长 + 1 成为 P[0…i] 的最长长度); ② 回退后 j=0 且 P[i] != P[j]:P[0…i] 无相等真前后缀,next[i]=0,符合定义。

实例验证:模式串 AAAB,i=3(P[3]=B),j=next[2]=2(P[0…2] 的最长长度 2):

  • P[3] = B != P[2] = A → 回退 j=next[1] = 1;
  • P[3] = B != P[1] = A → 回退 j=next[0] = 0;
  • j = 0 且 P[3] != P[0] → next[3] = 0;
  • P[0…3](AAAB)无相等真前后缀,符合定义。

5. 回退逻辑的'最优性'(为什么不从头找?)

迭代法中 j = next[j-1] 的回退逻辑,是正确性的关键补充 —— 它保证了'每次回退都找当前最优的次长长度',而非从头枚举(如暴力法),既高效又正确:

  1. 无重复验证:利用已计算的 next 值(归纳假设已证明正确),直接跳转到次长长度,避免重复验证;
  2. 最优性:next[j-1] 是 P[0…j-1] 的最长长度,因此推导得到的是 P[0…i-1] 的次长长度(若存在更长的,早就在之前的迭代中被找到);
  3. 终止条件:回退到 j=0 时,已穷尽所有可能的次长长度,此时直接判断 P[i] 与 P[0] 是否匹配即可。

6. 实例计算 next 数组(手把手教)

  • 以模式串 P='AAAB'(m=4)为例,一步步计算 next 数组,让你彻底掌握:
    • 初始化:next[0] = 0,i=1,j=0。
  1. i=1,P[1]=A,P[j]=P[0]=A:相等,j=1,next[1]=1,i=2;
  2. i=2,P[2]=A,P[j]=P[1]=A:相等,j=2,next[2]=2,i=3;
  3. i=3,P[3]=B,P[j]=P[2]=A:不相等,j=next[1]=1; 回退后 P[3]=B != P[1]=A,j=next[0]=0; 回退后 P[3]=B != P[0]=A,next[3]=0,i=4(遍历结束)。
  • 最终 next 数组:[0,1,2,0]。

  • 在以模式串 P='abcabd'(m=6)为例,一步步计算 next 数组,让你彻底掌握:

    • 初始化:next[0] = 0,i=1,j=0。
  1. i=1,P[1]=b,P[j]=P[0]=a:不相等,j=0,next[1]=0,i=2;
  2. i=2,P[2]=c,P[j]=a:不相等,j=0,next[2]=0,i=3;
  3. i=3,P[3]=a,P[j]=a:相等,j=1,next[3]=1,i=4;
  4. i=4,P[4]=b,P[j]=P[1]=b:相等,j=2,next[4]=2,i=5;
  5. i=5,P[5]=d,P[j]=P[2]=c:不相等,j=next[1]=0; 回退后 P[5]=d != P[0]=a,next[5]=0,i=6(遍历结束);
  • 最终 next 数组:[0,0,0,1,2,0]。

7. C++ 代码实现

// 生成模式串的 next 数组
std::vector<int> getNext(const std::string& p){
    int m = p.size();
    std::vector<int> next(m, 0); // next 数组初始化为 0,next[0] 固定为 0
    int j = 0; // 前缀指针,记录最长相等前后缀的长度
    for(int i = 1; i < m;++i){ // 后缀指针 i 从 1 开始遍历
        // 情况 2:P[i] != P[j],回退 j,直到 j=0 或匹配成功
        while(j > 0 && p[i] != p[j]){ 
            j = next[j - 1];
        }
        // 情况 1:P[i] == P[j],j++,更新 next[i]
        if(p[i] == p[j]){ 
            j++;
        } 
        next[i] = j;
    }
    return next;
}

四、KMP 算法

1. KMP 的匹配逻辑(核心)

主串 S 指针 i、模式串 P 指针 j 初始均为 0,遍历主串进行匹配:

  • 若 S[i] == P[j]:i++,j++,继续匹配;
  • 若 S[i] != P[j]:
    • 若 j > 0:j = next[j-1](利用 next 数组回退 j,不回溯 i);
    • 若 j == 0:i++(模式串第一个字符就不匹配,主串指针后移);
  • 终止条件:
    • 若 j == m(m 为模式串长度):匹配成功,返回起始索引 i-j;
    • 若 i == n(n 为主串长度)且 j < m:匹配失败,返回 -1。

2. KMP 匹配过程

实例 1:主串 S = 'AAAAAAB',模式串 P = 'AAAB'

匹配前准备

  • 主串 S='AAAAAAB'(长度 n=7)
  • 模式串 P='AAAB'(长度 m=4)
  • 已计算好的 next 数组:next = [0,1,2,0];
  • 初始化:i=0(主串指针,永不回溯),j=0(模式串指针,匹配失败时通过 next 数组回退)。

逐步骤匹配过程

  1. i=0,j=0:S[0]=A,P[0]=A:相等,i=1,j=1;
  2. i=1,j=1:S[1]=A,P[1]=A:相等,i=2,j=2;
  3. i=2,j=2:S[2]=A,P[2]=A:相等,i=3,j=3;
  4. i=3,j=3:S[3]=A,P[3]=B:不相等,触发 j 回退;j = next[j-1] = next[2] = 2(核心:利用 next 数组回退,而非重置为 0);
  5. i=3,j=2:S[3]=A,P[2]=A:相等,i=4,j=3;
  6. i=4,j=3:S[4]=A,P[3]=B:不相等,触发 j 回退;j = next[j-1] = next[2] = 2;
  7. i=4,j=2:S[4]=A,P[2]=A:相等,i=5,j=3;
  8. i=5,j=3:S[5]=A,P[3]=B:不相等,触发 j 回退;j = next[j-1] = next[2] = 2;
  9. i=5,j=2:S[5]=A,P[2]=A:相等,i=6,j=3;
  10. i=6,j=3:S[6]=B,P[3]=B:相等,i=7,j=4;此时 j=m=4(模式串全部匹配完成),匹配成功;起始索引 = i - m = 7 - 4 = 3;

动图演示

在这里插入图片描述

实例 2:主串 S = 'abcabcabdabc',模式串 P = 'abcabd'

匹配前准备

  • 主串 S='abcabcabdabc'(长度 n=11)
  • 模式串 P='abcabd'(长度 m=6)
  • 已计算好的 next 数组:next = [0,0,0,1,2,0];
  • 初始化:i=0(主串指针,永不回溯),j=0(模式串指针,匹配失败时通过 next 数组回退)。

逐步骤匹配过程

  1. i=0,j=0:S[0]=a,P[0]=a:相等,i=1,j=1;
  2. i=1,j=1:S[1]=b,P[1]=b:相等,i=2,j=2;
  3. i=2,j=2:S[2]=c,P[2]=c:相等,i=3,j=3;
  4. i=3,j=3:S[3]=a,P[3]=a:相等,i=4,j=4;
  5. i=4,j=4:S[4]=b,P[4]=b:相等,i=5,j=5;
  6. i=5,j=5:S[5]=c,P[5]=d:不相等,触发 j 回退;j = next[j-1] = next[4] = 2(核心:利用 next 数组回退,而非重置为 0);
  7. i=5,j=2:S[5]=c,P[2]=c:相等,i=6,j=3;
  8. i=6,j=3:S[6]=a,P[3]=a:相等,i=7,j=4;
  9. i=7,j=4:S[7]=b,P[4]=b:相等,i=8,j=5;
  10. i=8,j=5:S[8]=d,P[5]=d:相等,i=9,j=6;此时 j=m=6(模式串全部匹配完成),匹配成功;起始索引 = i - m = 9 - 6 = 3;

动图演示

在这里插入图片描述

3. C++ 代码实现

// KMP 匹配主函数:返回模式串 p 在主串 s 中首次出现的起始索引,失败返回 -1
int kmpSearch(const std::string& s, const std::string& p){
    int n = s.size();
    int m = p.size();
    if(m == 0) return 0; // 模式串为空,默认匹配成功
    std::vector<int> next = getNext(p);
    int j = 0; // 模式串指针
    for(int i = 0; i < n;++i){ // 主串指针 i 不回溯,一直前进
        // 匹配失败,根据 next 数组回退 j
        while(j > 0 && s[i] != p[j]){ 
            j = next[j - 1];
        }
        // 匹配成功,模式串指针前进
        if(s[i] == p[j]){ 
            j++;
        }
        // 模式串全部匹配成功,返回起始索引
        if(j == m){ return i - m + 1; }
    }
    return -1; // 匹配失败
}

五、复杂度分析

KMP 算法的时间复杂度分为 next 数组生成的时间复杂度和核心匹配的时间复杂度,空间复杂度为 next 数组的存储空间。

1. 时间复杂度

  • next 数组生成:遍历模式串一次,长度为 m。内层 while 循环中,j 的回退总次数不超过 j 的前进总次数,因此内层循环总操作数为 O(m),整体生成 next 数组的时间复杂度为 O(m)。
  • KMP 核心匹配:遍历主串一次,长度为 n。同理,内层 while 循环中 j 的回退总次数不超过 j 的前进总次数,内层循环总操作数为 O(m),整体匹配的时间复杂度为 O(n)。
  • 算法总时间复杂度:O(n + m),线性时间复杂度,远优于暴力匹配的 O(n*m)。

2. 空间复杂度

KMP 算法需要额外存储 next 数组,数组长度为模式串长度 m,因此空间复杂度为 O(m)。

六、KMP 算法的应用场景

KMP 算法作为经典的线性字符串匹配算法,在工程中应用广泛,典型场景包括:

  1. 文本编辑器的查找功能(如 Notepad++ 的'查找');
  2. 编译器的词法分析(匹配关键字、标识符);
  3. 生物信息学(DNA 序列、蛋白质序列的匹配);
  4. 网络协议解析(匹配固定的协议字段);
  5. 面试高频考点(字符串匹配类题目必问,如 LeetCode 28. 找出字符串中第一个匹配项的下标)。

总结

KMP 算法的核心是'利用已匹配信息,避免重复比较',而 next 数组是实现这一思想的关键,记住以下 3 个核心点,就能彻底掌握 KMP:

  1. 最长公共前后缀:next 数组的本质,是模式串每个位置的最长相等真前后缀长度;
  2. 主串指针不回溯:KMP 的核心改进,仅通过模式串指针 j 的回退实现匹配;
  3. next 数组的迭代生成:通过前缀指针 j 和后缀指针 i 的遍历,结合回退逻辑,线性时间生成 next 数组。

目录

  1. 前言
  2. 一、算法概述
  3. 1. 解决的问题
  4. 2. 传统暴力匹配的问题
  5. 3. KMP 算法的核心改进
  6. 二、算法核心原理
  7. 关键概念:最长公共前后缀(真前后缀)
  8. 三、next 数组计算详解(KMP 的灵魂)
  9. 1. 为什么需要 next?
  10. 2. next 的暴力方法求解
  11. 3. next 数组的计算思路(迭代法)
  12. 4. 迭代法的正确性分析
  13. 5. 回退逻辑的“最优性”(为什么不从头找?)
  14. 6. 实例计算 next 数组(手把手教)
  15. 7. C++ 代码实现
  16. 四、KMP 算法
  17. 1. KMP 的匹配逻辑(核心)
  18. 2. KMP 匹配过程
  19. 3. C++ 代码实现
  20. 五、复杂度分析
  21. 1. 时间复杂度
  22. 2. 空间复杂度
  23. 六、KMP 算法的应用场景
  24. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 基于 SpringBoot 的医院住院管理系统设计与实现
  • Verilog 入门:语法、仿真与 FPGA 实战
  • Git 配置与使用详解
  • JavaWeb 核心:JSON 数据交换与 Ajax 异步请求详解
  • AI 技能(Skills):面向任务自动化的模块化执行范式
  • Java 21 虚拟线程(Virtual Threads)使用指南
  • AI 大模型应用后端开发零基础入门路线
  • 算法:双指针解法 - 复写零
  • GitHub Copilot 自定义指令配置实战:让 AI 遵循团队规范
  • WuliArt Qwen-Image Turbo 本地部署实战指南
  • OpenClaw 自托管 AI 网关安装部署全流程指南
  • 从零实现C++调试器的核心原理与Windows API实战
  • ZeroClaw 本地 AI 管理面板:Gateway + LM Studio + Reflex
  • 开源 ROS 智能割草机器人:硬件选型、软件架构与部署指南
  • VS Code 配置 C/C++ 时报错 preLaunchTask 生成活动文件已终止且退出代码为 -1 的解决方法
  • MacOS 多合一启动盘制作与系统降版本指南
  • C++ 高并发内存池:基于基数树的性能优化与测试
  • VSCode 与 PyCharm 配置 OpenCV 教程(Python 与 C++)
  • WorkBuddy 7 种 IM 渠道接入指南:AI 助手配置与使用
  • 低代码选型指南:AI 与低代码双向赋能企业数字化

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online