【数据结构】字符串模式匹配:暴力算法与 KMP 算法实现与解析

在编程开发中,字符串模式匹配是非常基础且常用的操作,核心需求是在一个主字符串(str)中查找指定子字符串(pattern,模式串)的首次出现位置。本文将介绍两种经典的模式匹配算法 ------ 暴力匹配算法和 KMP 算法,通过 C 语言代码实现深入解析其原理、执行过程及优缺点,帮助大家理解两种算法的核心差异。

一、暴力匹配算法(BF 算法)

暴力匹配算法(Brute Force)也叫朴素匹配算法,是最简单的字符串模式匹配方法,核心思路是逐位比对,失配回溯,通过两层循环依次尝试主字符串中所有可能的起始位置,与模式串进行匹配。

1. 算法核心原理

  1. 设主字符串长度为n,模式串长度为m,主字符串的匹配起始位置i从 0 开始,最大到n-m(超出后剩余字符不足以匹配模式串)。
  2. 模式串的比对指针j从 0 开始,逐位比较str[i]pattern[j]
  3. 若字符相等,ij同时后移,继续比对下一位;若字符不相等,主字符串指针回溯 到本次起始位置的下一位(i = i - j),模式串指针j重置为 0,重新开始匹配。
  4. 若模式串指针j遍历完所有字符(j == m),说明匹配成功,返回本次匹配的起始位置(i - j);若主字符串遍历完仍未匹配,返回 - 1。

2. C 语言代码实现

cpp 复制代码
#include <stdio.h>
#include <string.h>

/**
 * 暴力字符串模式匹配函数
 * @param str     主字符串(被查找的长串)
 * @param pattern 模式串(要查找的短串)
 * @return        找到:返回模式串在主串中第一次出现的起始下标
 *                没找到:返回 -1
 */
int strMatch(char* str, char* pattern)
{
    // 获取主字符串的长度
    int n = strlen(str);
    // 获取模式串的长度
    int m = strlen(pattern);

    // 外层循环:控制主串的【匹配起始位置】i
    // 主串最多只能从 n-m 位置开始匹配,否则剩余字符不够匹配模式串
    for (int i = 0; i <= (n - m); i++)
    {
        // 每次重新匹配,模式串指针 j 都从 0 开始
        int j = 0;

        // 内层循环:逐字符对比
        while(j < m)
        {
            // 如果当前主串字符 == 模式串字符,两个指针同时向后移动
            if (str[i] == pattern[j])
            {
                i++;  // 主串指针后移
                j++;  // 模式串指针后移
            }
            // 字符不相等,发生失配,需要回溯
            else
            {
                // 关键:主串 i 回溯到【本次匹配起始位置的下一位】
                i = i - j;
                // 退出内层循环,重新开始下一轮匹配
                break;
            }
        }

        // 如果 j 走完了整个模式串,说明完全匹配成功
        if (j == m)
        {
            // 返回匹配的起始位置(i - j 是因为 i 已经后移过了)
            return i - j;
        }
    }

    // 循环结束都没找到,返回 -1
    return -1;
}

// 主函数:测试暴力匹配算法
int main(int argc, char const *argv[])
{
    // 定义主串
    char* str = "abcabaabcabc";
    // 定义要查找的模式串
    char* pattern = "abaa";
    // 调用匹配函数
    int pos = strMatch(str, pattern);
    // 输出结果:找到则输出下标,没找到输出 -1
    printf("模式串出现的位置:%d\n", pos);
    return 0;
}

3. 算法特点

  • 优点:逻辑简单,代码实现直观,无需额外的预处理步骤,适合短字符串的简单匹配场景。
  • 缺点 :效率低下,存在大量的无效回溯 。当主字符串和模式串存在部分匹配的前缀时,主字符串指针需要回退到初始位置,重新比对,最坏时间复杂度为O(n*m)(n 为主串长度,m 为模式串长度)。
  • 测试结果 :上述代码中主串abcabaabcabc与模式串abaa无匹配,最终输出-1

二、KMP 算法

KMP 算法由 Knuth、Morris、Pratt 三位学者提出,是对暴力匹配算法的优化,核心解决了暴力算法中主字符串指针回溯 的问题,通过对模式串进行预处理生成 next 数组,记录模式串失配时的回退位置,让主字符串指针始终只向后移动,大幅提升匹配效率。

1. 算法核心原理

KMP 算法的核心是next 数组 ,next 数组的长度与模式串一致,next[j]表示模式串中第j位字符失配时,模式串指针需要回退到的位置,本质是找模式串前缀和后缀的最长相等子串长度。整个算法分为两个阶段:

  1. 预处理阶段:遍历模式串,生成 next 数组,记录每个位置的失配回退点。
  2. 匹配阶段 :主串指针i和模式串指针j均从 0 开始,逐位比对;若失配,根据 next 数组让模式串指针j回退,主串指针i保持不变;若匹配成功,两个指针同时后移,直到匹配完成或主串遍历结束。

2. 关键:next 数组的生成

next 数组的生成规则(本文实现为基础版 next 数组):初始化next[0] = -1(模式串首位失配,主串指针后移,模式串指针回退到 -1,这是人为定义的边界条件),同时定义两个指针:i为模式串的遍历指针,从 0 开始,负责逐个遍历模式串字符;j为前缀后缀匹配指针,初始值为 - 1,负责记录当前位置前的模式串中,前缀和后缀的最长相等子串长度,也是失配时的回退目标位置。

随后进入循环遍历模式串(i < 模式串长度m),核心逻辑分两种情况:

  1. j == -1pattern[i] == pattern[j]j == -1表示当前回到匹配起点,无任何前缀后缀可匹配;而pattern[i] == pattern[j]表示当前字符匹配成功,说明找到更长的相等前后缀。此时将ij同时向后移动一位,再将next[i]赋值为j,这个j值就是模式串第i位字符失配时,需要回退到的目标位置。
  2. pattern[i] != pattern[j] :说明当前字符匹配失败,无法形成更长的相等前后缀,此时需要让j根据已生成的 next 数组向前回退 ,即j = next[j],重新寻找更短的相等前后缀,直到满足j == -1或字符匹配成功为止。

整个 next 数组的生成过程,本质是模式串自己和自己做匹配 ,通过不断寻找前后缀的最长相等子串,提前记录所有位置的失配回退点,为后续的主串匹配阶段做预处理,这一步的时间复杂度为O(m)(m 为模式串长度)。

3. C 语言代码实现(带完整详细注释)

cpp 复制代码
#include <stdio.h>
#include <string.h>

/**
 * 预处理模式串,生成KMP核心的next数组
 * @param pattern 待处理的模式串
 * @param next    用于存储回退位置的next数组
 */
void getNext(char* pattern, int* next)
{
    int m = strlen(pattern); // 获取模式串长度
    int i = 0;               // 模式串遍历指针,从0开始
    int j = -1;              // 前后缀匹配指针,初始为-1(边界条件)
    next[0] = -1;            // next数组首位固定为-1,人为定义的失配边界

    // 遍历模式串,生成完整的next数组(i < m 避免数组越界)
    while(i < m)
    {
        // 边界情况 或 当前字符匹配成功,拓展相等前后缀
        if (j == -1 || pattern[i] == pattern[j])
        {
            i++;    // 模式串遍历指针后移
            j++;    // 前后缀匹配长度+1,回退位置后移
            next[i] = j; // 记录当前位置的失配回退点
        }
        else
        {
            j = next[j]; // 字符匹配失败,j根据next数组向前回退
        }
    }
}

/**
 * KMP算法核心匹配函数
 * @param str     主字符串(被查找的长串)
 * @param pattern 模式串(要查找的短串)
 * @return        匹配成功:返回模式串在主串中首次出现的起始下标
 *                匹配失败:返回-1
 */
int kmp(char* str, char* pattern)
{
    int i = 0;               // 主串遍历指针,从不回溯,仅向后移动
    int j = 0;               // 模式串遍历指针,失配时根据next回退
    int next[100];           // 定义next数组,存储模式串失配回退点
    getNext(pattern, next);  // 预处理模式串,生成next数组

    int n = strlen(str);     // 获取主串长度
    int m = strlen(pattern); // 获取模式串长度

    // 主串和模式串均未遍历完时,继续匹配
    while(i < n && j < m)
    {
        // 模式串回退到边界 或 当前主串与模式串字符匹配成功
        if (j == -1 || str[i] == pattern[j])
        {
            i++; // 主串指针后移,继续匹配下一位
            j++; // 模式串指针后移,继续匹配下一位
        }
        else
        {
            j = next[j]; // 字符失配,主串指针不动,模式串指针按next回退
        }
    }

    // 若模式串指针遍历完所有字符,说明完全匹配成功
    if (j == m)
    {
        return i - j; // 计算并返回模式串在主串中的起始下标
    }
    else
    {
        return -1; // 匹配失败,返回-1
    }
}

// 主函数:测试KMP算法匹配效果
int main(int argc, char const *argv[])
{
    char* str = "abaabaabacacaabaabcc";  // 定义主字符串
    char* pattern = "abaabc";            // 定义要查找的模式串
    printf("%d\n", kmp(str, pattern));   // 调用KMP函数,打印匹配结果
    return 0;
}

4. 算法匹配执行示例

以上述测试代码为例,主串为abaabaabacacaabaabcc,模式串为abaabc,经getNext函数预处理后,生成的 next 数组为[-1,0,0,1,2,0]

匹配过程中,主串指针i始终向后移动,当遇到字符失配时,模式串指针j根据 next 数组回退到对应位置(而非重置为 0),最终会在主串下标 5 的位置匹配到模式串,因此代码运行结果输出5

5. 算法特点

  • 优点 :彻底解决了暴力算法的无效回溯问题,主串指针仅遍历一次,预处理阶段时间复杂度 O (m),匹配阶段时间复杂度 O (n),整体时间复杂度为 O (n+m)(n 为主串长度,m 为模式串长度),在长字符串匹配场景下效率远高于暴力算法。
  • 缺点 :基础版 next 数组存在少量冗余回退(可通过优化 next 数组为 nextval 数组解决);需要额外的数组空间存储 next 数组,空间复杂度为 O (m),属于空间换时间的经典算法设计思路。

三、暴力算法与 KMP 算法核心对比

对比维度 暴力匹配算法(BF) KMP 算法
核心思路 逐位匹配,失配后主串、模式串指针均回溯 预处理模式串生成 next 数组,失配仅模式串指针回退,主串指针不回溯
时间复杂度 最坏 O (n*m),最好 O (n) 整体 O (n+m),无最坏情况,效率稳定
空间复杂度 O (1),无需额外辅助空间 O (m),需要存储 next 数组
实现难度 逻辑简单,代码易实现 需理解 next 数组原理,实现稍复杂
适用场景 短字符串、简单匹配场景,对效率要求不高 长字符串、高频匹配场景,对效率要求较高

四、408真题演练

KMP 算法的核心是next 数组 ,失配时通过next数组回退模式串指针j,主串指针i保持不变。

  1. 确定模式串tt = "abaababc",索引从 0 开始:

    索引 j 0 1 2 3 4 5 6 7
    字符 t [j] a b a a b a b c
  2. 计算模式串的next数组next[j]表示t[0..j-1]的最长公共前后缀长度。

    • next[0] = -1(约定)
    • next[1] = 0(子串"a"无前后缀)
    • next[2] = 0(子串"ab"
    • next[3] = 1(子串"aba",最长公共前后缀为"a"
    • next[4] = 2(子串"abaa",最长公共前后缀为"ab"
    • next[5] = 2(子串"abaab",最长公共前后缀为"ab"
    • next[6] = 3(子串"abaaba",最长公共前后缀为"aba"
    • next[7] = 4(子串"abaabab",最长公共前后缀为"abab"

    完整next数组:[-1, 0, 0, 1, 2, 2, 3, 4]

  3. 失配处理 :题目中第一次失配时 i=5, j=5,根据 KMP 规则:

    • 主串指针i保持不变,即i=5
    • 模式串指针j回退到next[j] = next[5] = 2


答案:C

步骤 1:计算模式串 Snext 数组

模式串 S = "abaabc",索引从 0 开始:

索引 j 0 1 2 3 4 5
字符 S [j] a b a a b c

next[j] 定义为子串 S[0..j-1] 的最长公共前后缀长度,约定 next[0] = -1

  • next[0] = -1
  • next[1] = 0(子串 "a" 无前后缀)
  • next[2] = 0(子串 "ab"
  • next[3] = 1(子串 "aba",最长公共前后缀为 "a"
  • next[4] = 2(子串 "abaa",最长公共前后缀为 "ab"
  • next[5] = 2(子串 "abaab",最长公共前后缀为 "ab"

最终 next 数组:[-1, 0, 0, 1, 2, 2]


步骤 2:逐次模拟 KMP 匹配过程

主串 T = "abaabaab cababaabc",模式串 S = "abaabc",初始化 i=0(主串指针)、j=0(模式串指针),比较次数 count=0

  1. 第 1~6 次比较

    • T[0]=a vs S[0]=a → 匹配,i=1,j=1count=1
    • T[1]=b vs S[1]=b → 匹配,i=2,j=2count=2
    • T[2]=a vs S[2]=a → 匹配,i=3,j=3count=3
    • T[3]=a vs S[3]=a → 匹配,i=4,j=4count=4
    • T[4]=b vs S[4]=b → 匹配,i=5,j=5count=5
    • T[5]=a vs S[5]=c → 失配,count=6
    • 失配处理:j = next[5] = 2i 保持 5
  2. 第 7 次比较

    • T[5]=a vs S[2]=a → 匹配,i=6,j=3count=7
  3. 第 8~11 次比较

    • T[6]=a vs S[3]=a → 匹配,i=7,j=4count=8
    • T[7]=b vs S[4]=b → 匹配,i=8,j=5count=9
    • T[8]=c vs S[5]=c → 匹配,i=9,j=6count=10
    • 此时 j=6 等于模式串长度 6,匹配成功 ✅

答案:B

五、总结

字符串模式匹配中,暴力算法是最基础的实现方式,凭借逻辑简单的优势,在短字符串匹配中仍有一定应用;而 KMP 算法通过预处理生成 next 数组的巧妙设计,用少量的空间开销换来了时间效率的大幅提升,解决了暴力算法的无效回溯问题,成为长字符串模式匹配的经典算法。

理解 KMP 算法的关键在于掌握next 数组的生成原理------ 本质是模式串的自匹配,记录每个位置的最长相等前后缀长度,这也是 KMP 算法的核心精髓。掌握这两种算法,能为后续学习更高效的字符串匹配算法(如 BM 算法、Sunday 算法)打下坚实的基础。

相关推荐
客卿1232 小时前
动态规划--模板--完全背包
算法·动态规划
L-影2 小时前
下篇:一棵树能长成多少种样子?——AI中决策树的类型与作用,以及它凭什么活了六十年还没过气
人工智能·算法·决策树·ai
mifengxing2 小时前
力扣HOT100——(1)两数之和
java·数据结构·算法·leetcode·hot100
無限進步D2 小时前
算竞常用STL cpp
开发语言·c++·算法·竞赛
仟濹2 小时前
【算法打卡day34(2026-03-30 周一)】DFS专项训练(今日算法:DFS & 记忆化搜索 & 回溯)
算法·深度优先
罗湖老棍子2 小时前
【 例 1】区间和(信息学奥赛一本通- P1547)(基础线段树和单点修改区间查询树状数组模版)
数据结构·算法·线段树·树状数组·单点修改 区间查询
Book思议-3 小时前
【数据结构】栈与队列核心对比
数据结构·栈与队列对比
旺仔.2913 小时前
常用算法 详解
数据结构·算法
今儿敲了吗3 小时前
算法复盘——差分
数据结构·c++·笔记·学习·算法