字符串匹配:BF算法与KMP算法

一个人能走多远不在于他在顺境时能走得多快,而在于他在逆境时多久能找到曾经的自己。

------ KMP

字符串匹配作为计算机领域最核心的基础算法之一,广泛应用于文本检索、编译器词法分析、网络数据解析、病毒特征匹配等各类实际开发场景中。高效的字符串匹配算法能够大幅提升程序运行效率,降低时间损耗,而 KMP 算法正是解决字符串匹配问题的经典高效算法,它彻底解决了暴力匹配算法的效率缺陷。


一、字符串匹配问题

在程序开发中,字符串匹配是最常见的需求之一:给定一个主串 (目标字符串)和一个子串 (模式字符串),我们需要完成的核心任务是,判断子串是否作为一个连续片段存在于主串当中。 如果存在,返回子串在主串中第一次出现的起始下标;如果不存在,则返回 - 1 作为匹配失败的标识。 这个问题看似简单,却是整个字符串处理领域的基础,而解决这个问题的算法,从最朴素的暴力枚举,到高效的 KMP 算法,体现了算法优化的核心思想。


二、BF 算法(暴力枚举法)

2.1 BF 算法描述

BF 算法全称 Brute Force,即暴力匹配算法,是字符串匹配问题中最直观、最容易理解的解法,它不依赖任何复杂的预处理逻辑,完全依靠逐字符比对完成匹配。 算法执行流程:

  • 定义两个下标变量iji用于遍历主串,初始指向主串起始位置,j用于遍历子串,初始指向子串起始位置
  • 进入循环匹配,循环的核心条件是ij均未超出对应字符串的有效长度,保证比对的字符是合法有效的
  • 若当前ij指向的字符相等,两个指针同步向后移动一位,继续比对下一个字符
  • 若当前字符不相等,匹配失败,i回退到本趟匹配起始位置的下一个字符,j直接回退到子串起始位置 0,重新开始新一轮匹配
  • 循环结束后,通过j的值判断匹配结果:若j遍历完整个子串,说明匹配成功,返回起始下标;否则匹配失败,返回 - 1

BF 算法的核心缺陷十分明显:每次匹配失败后,主串指针i需要大量回退,造成了大量重复的字符比对操作,在数据量较大时,算法效率极低。 时间复杂度 :最坏情况下为O(n*m)(n 为主串长度,m 为子串长度)。

2.2 BF 算法实现

cpp 复制代码
// BF暴力匹配算法实现
int BF_Search(const char* des, const char* sub) {
    // 断言校验,确保传入的字符串指针非空,避免空指针访问
    assert(des != nullptr);
    assert(sub != nullptr);
    int i = 0, j = 0;
    int des_len = strlen(des);
    int sub_len = strlen(sub);
    // 循环条件:主串和子串指针均未越界
    while (i < des_len && j < sub_len) {
        if (des[i] == sub[j]) {
            // 字符匹配,两个指针同时后移
            i++;
            j++;
        } else {
            // 匹配失败,i回退,j重置为0
            i = i - j + 1;
            j = 0;
        }
    }
    // j遍历完子串,说明匹配成功
    if (j == sub_len)
        return i - j;
    else
        // 匹配失败
        return -1;
}

三、KMP 算法

3.1 KMP 算法思想

KMP 算法是对 BF 暴力算法的颠覆性优化 ,它的核心突破点在于:主串指针i永远不回退 ,仅通过调整子串指针j的位置完成匹配,彻底消除了重复比对的操作,将算法时间复杂度优化到O(n+m),在长文本匹配场景中优势极其显著。

KMP 算法的核心依托是next 数组 ,这个数组是针对子串预处理得到的,存储了子串中每个字符失配时,j应该回退的最优下标,无需让i回退重新匹配,直接跳过不可能成功的匹配位置。

3.2 为什么主串指针i可以不回退?

当匹配发生失败时,我们只需要关注子串失配位置之前的字符片段:

  • 若该片段中存在最长相等的前缀和后缀 (左橙 = 右橙),i的回退没有任何实际意义,这种回退必然会导致匹配失败,完全可以通过j跳转到 next 数组指定的位置替代
  • 若该片段中不存在相等的前缀和后缀,那么i无论如何回退,都无法完成匹配,因此i更没有回退的必要
  • 综上,无论子串失配位置前是否存在相等前后缀,主串指针i都可以保持不回退,这是 KMP 算法高效的核心逻辑。

3.3 KMP 算法实现

cpp 复制代码
// KMP匹配算法实现
int KMP_Search(const char* des, const char* sub);
// 获取next数组
int* Get_Next(const char* sub);

int main() {
    const char arr[] = "aaabccc";
    const char brr[] = "abc";
    // 调用KMP算法进行匹配
    int res = KMP_Search(arr, brr);
    if (res == -1)
        cout << "匹配失败,未找到目标子串" << endl;
    else
        cout << "匹配成功,子串起始下标为:" << res << endl;

    return 0;
}

int KMP_Search(const char* des, const char* sub) {
    // 合法性校验
    assert(des != nullptr);
    assert(sub != nullptr);
    int i = 0, j = 0;
    int des_len = strlen(des);
    int sub_len = strlen(sub);
    // 为子串生成next数组,仅需计算一次
    int* next = Get_Next(sub);
    // 主循环匹配,i不回退
    while (i < des_len && j < sub_len) {
        // j=-1表示退无可退,或字符匹配成功,指针后移
        if (j == -1 || des[i] == sub[j]) {
            i++;
            j++;
        } else {
            // 匹配失败,j根据next数组回退到最优位置
            j = next[j];
        }
    }
    // 释放动态申请的next数组内存,防止内存泄漏
    free(next);
    // 判断匹配结果
    if (j == sub_len)
        return i - j;
    else
        return -1;
}

四、Next 数组

4.1 Next 数组定义

Next 数组是专门针对子串预处理生成的辅助数组,具备两个特征:

  • 数组长度与子串的字符个数完全一致,一一对应子串的每一个位置
  • 数组中存储的数值,代表子串在当前下标位置发生失配时,j指针需要回退的最优下标位置

Next 数组的本质,是寻找子串中每个位置最长相等前缀和后缀的长度 ,这个长度决定了j的回退位置,能够最大程度减少无效匹配。

4.2 Next 数组求解

第一种:直接寻找最长相等前后缀(左橙 = 右橙),前缀长度即为对应位置的 next 值。

第二种:用已知求未知,这是代码实现的核心方法:利用已经计算完成的 next 值,推导下一个位置的 next 值。 核心规则:如果当前字符与回退位置的字符相同,说明前后缀可以延长一位,next 值 + 1;如果不同,则继续回退,直到找到匹配位置或退至 - 1。

4.3 Next 数组实现

cpp 复制代码
// 生成子串对应的next数组
int* Get_Next(const char* sub) {
    // 校验子串合法性
    assert(sub != nullptr);
    int len = strlen(sub);
    // 动态分配内存,存储next数组
    int* Next = (int*)malloc(len * sizeof(int));
    // 内存分配失败,直接退出程序
    if (Next == nullptr)
        exit(EXIT_FAILURE);
    // 手动初始化前两个位置的next值,固定规则
    Next[0] = -1;
    if (len > 1)
        Next[1] = 0;

    int j = 1;
    int k = 0;
    // 循环计算剩余位置的next值
    while (j + 1 < len) {
        // 回退到起点,或字符匹配,next值+1
        if (k == -1 || sub[k] == sub[j]) {
            Next[++j] = ++k;
        } else {
            // 不匹配,继续回退k
            k = Next[k];
        }
    }
    return Next;
}

五、Nextval 数组

Nextval 数组是对 Next 数组的进阶优化,它解决了原始 Next 数组中存在的无意义回退问题,进一步减少 KMP 算法的匹配次数,让算法效率更高。

  • 如果当前位置的字符,与它 Next 数组指向的回退位置字符不相同,说明本次回退是有价值的,直接将当前位置的 Next 值赋值给 Nextval
  • 如果当前位置的字符,与它 Next 数组指向的回退位置字符相同,说明本次回退没有意义,匹配依然会失败,直接将回退位置的 Nextval 值赋值给当前位置

简单来说,Nextval 数组会跳过所有重复的、无意义的回退操作,让j指针一步到位跳转到最终的有效位置,避免了多余的字符比对,是 KMP 算法的终极优化形态。

六、算法总结

BF 算法 vs KMP 算法

Next 数组 vs Nextval 数组

相关推荐
wandertp1 小时前
对信号处理及滤波器的理解---基于robomaster机器人嵌入式控制系统
arm开发·stm32·算法·信号处理
z小猫不吃鱼1 小时前
15 InstructGPT 论文精读:SFT + RLHF 如何让模型听懂指令?
人工智能·深度学习·算法·机器学习·语言模型·自然语言处理·gpt-3
见合八方2 小时前
【滤波器】热调谐FP滤波器
人工智能·算法
古城小栈2 小时前
cargo-pprof:Rust性能调优
人工智能·算法·rust
x_xbx2 小时前
LeetCode:543. 二叉树的直径
算法·leetcode·职场和发展
QiLinkOS2 小时前
QiLink 技术委员会选举实施细则
c语言·数据结构·c++·单片机·嵌入式硬件·算法·开源
我材不敲代码2 小时前
Python基础: 函数超全详解:定义、参数、返回值、作用域与递归
开发语言·python·算法
无忧.芙桃2 小时前
数据结构之顺序表的实现
数据结构
罗超驿2 小时前
11.LeetCode 1004. 最大连续1的个数 III | 滑动窗口解法详解(Java)
java·算法·leetcode