字符串模式匹配(朴素模式匹配算法与KMP算法)

文章目录

什么是字符串的模式匹配

在主串 N 中,查找 M 子串。(N>>M 主串的长度可能远大于子串的长度)

两种模式匹配算法

字符串模式匹配 :在主串中找到与模式串相同的子串,并返回其(第一个元素)所在位置。

子串 是主串的一部分,一定存在(于主串中);

模式串,不一定能在主串中找到。

第一种:朴素模式匹配算法(BF)

主串长度为 n,模式串长度为 m

朴素模式匹配算法:将主串中所有长度为m的子串最多对比n-m+1个子串)依次与模式串对比,直到找到一个完全匹配的子串(返回其第一个元素的位置位序),或将所有的子串都不匹配为止。

算法思想:"枚举所有可能的位置,逐一尝试匹配。"

  • 主串长度为 n,模式串长度为 m
  • 算法从主串的第1个字符开始,截取长度为 m 的子串与 T 比较。
  • 若匹配,则返回当前位置;若不匹配,则从主串的下一个位置重新开始比较。
  • 直到主串剩余长度不足 m,则匹配失败返回0

方式一:定位操作的方式实现

cpp 复制代码
// 定位操作
int Index(SString S, SString T) { // S为主串,T为子串
    int i = 1; 
    int n = S.length, m = T.length; // 获取串的长度
    SString Sub; // 用于暂存子串
    // 最多对比 n-m+1个子串
    while (i <= n - m + 1) { // 边界条件:剩余长度必须 >= m
        // 取出从位置i开始,长度为m的子串
        SubString(Sub, S, i, m); // 调用已实现的求子串 取出长度为m的子串
        // 子串和模式串对比,若不匹配,则匹配下一个子串
        if (StrCompare(Sub, T) == 0) {
            return i; // 匹配成功
        }
        i++; // 不匹配则往后移动一位继续查找
    }
    return 0; // 未找到 S中不存在与T相等的子串
}

方式二:数组下标实现朴素模式匹配算法

不使用字符串的基本操作 ,直接通过数组下标实现朴素模式匹配算法。

cpp 复制代码
int Index(SString S, SString T) {
    int i = 1, j = 1;     // i 遍历主串S, j 遍历子串T
    // 当i>S.length则结束循环
    while (i <= S.length && j <= T.length) {
        if (S.ch[i] == T.ch[j]) { // 1. 匹配成功,指针后移继续匹配
            ++i; ++j;
        } else {                  // 2. 匹配失败,i 回溯,j 重置
            i = i - j + 2;        //  核心回溯公式
            j = 1;
        }
    }
    if (j > T.length)            // 3. 子串全部匹配完
        return i - T.length;     // 返回起始位置
    else
        return 0;
}
时间复杂度

主串长度为n模式串长度为m ,则最坏的时间复杂度为O(nm).

第二种:KMP算法(朴素模式匹配的优化)

利用 不匹配的字符之前,前面的这些元素一定是和模式串一致的,可以直接选择从下一个不同的地方开始匹配。

** i (主串指针)不动(即主串指针i不用回溯),j变化**

求next数组(预处理)

根据模式串T,求出next数组。

next数组之和模式串有关,与主串无关。

cpp 复制代码
// 求模式串 T 的 next 数组(位序从1开始)
void get_next(SString T, int next[]) {
    int i = 1, j = 0;
    next[1] = 0; // 1.  next[0] 无脑写0

    while (i < T.length) { // 注意:i 从1遍历到 length-1,因为 next[length] 需要在循环内求
        if (j == 0 || T.ch[i] == T.ch[j]) { // 2. 匹配或退无可退
            ++i; ++j;
            next[i] = j; // 3. 记录当前 i 位置的 next 值
        } else {
            j = next[j]; // 4. 回溯 j(利用已求得的 next 数组)
        }
    }
}

模式匹配

利用next数组进行匹配(主串指针不回溯)

cpp 复制代码
// KMP 匹配算法(主串 S,模式串 T,已求好的 next 数组)
int Index_KMP(SString S, SString T, int next[]) {
    int i = 1, j = 1;
    while (i <= S.length && j <= T.length) {
        // 1. 若 j==0,表示第一个字符都不匹配,i 必须后移一位
        // 2. 若当前字符匹配,继续比较下一对
        if (j == 0 || S.ch[i] == T.ch[j]) {
            ++i; ++j;
        } else {
            // 3. 失配:i 不动,j 回溯到 next[j]
            j = next[j];
        }
    }
    if (j > T.length)
        return i - T.length; // 匹配成功
    else
        return 0;
}

时间复杂度

KMP算法,最坏时间复杂度:O(m+n)

  • 其中,预处理:求next数组时间复杂度:O(m)
  • 模式匹配:模式匹配过程最坏时间复杂度:O(n)

求模式串的next数组(手算练习)

  • next[1]都无脑写0
  • next[2]都无脑写1

其他next:

  1. 在不匹配的位置前,划一刀分界线;
  2. 模式串一步一步往后退(向右走),直到分界线之前(左边)都"能对上",或模式串完全跨过分界线为止。
  3. 此时,j 指向哪儿,则next数组值就是多少。

next数组的优化:nextval数组(手算练习)

cpp 复制代码
// 求优化后的 nextval 数组
void get_nextval(SString T, int nextval[]) {
    int i = 1, j = 0;
    nextval[1] = 0;

    while (i < T.length) {
        if (j == 0 || T.ch[i] == T.ch[j]) {
            ++i; ++j;
            // 核心优化:若 i 和 j 位置的字符相等,则继续递归
            if (T.ch[i] != T.ch[j]) {
                nextval[i] = j; // 不同,直接赋值
            } else {
                nextval[i] = nextval[j]; // 相同,用 nextval 覆盖
            }
        } else {
            j = nextval[j]; // 回溯时也用 nextval
        }
    }
}

手算解题:先求next数组,再由next数组求nextval数组。

cpp 复制代码
nextval[1] = 0;
// 从next[2]开始
for (int j = 2; j <= T.length; j++)
{
    if (T.ch[next[j]] == T.ch[j])
        nextval[j] = nextval[next[j]]; // 优化部分
    else
        nextval[j] = next[j];
}
相关推荐
dyxal1 小时前
eˣ 的泰勒展开式:从“化繁为简”到“万物统一”的微积分之美
算法
BetterFlow_CFD1 小时前
COMSOL声学仿真基础知识(二)
开发语言·算法·matlab
徐小夕13 小时前
花了一周,3亿tokens,我开源了一款 Word 文档智能审查平台,文稿自动质检+可视化分析,告别低效人工审核
前端·算法·github
可编程芯片开发14 小时前
UPFC统一潮流控制器的simulink建模与仿真
算法
小小仙子14 小时前
矢量网络分析仪如何测试S参数的?
人工智能·算法·机器学习
code bean15 小时前
【C#】 `Channel<T>` 深度解析:生产者-消费者模式的现代解法
数据结构·c#
中微极客16 小时前
降维算法75倍加速:从PCA到稀疏字典学习的工程实践
人工智能·学习·算法
storyseek16 小时前
前缀和实现Kogge-Stone算法
数据结构·算法