KMP算法

KMP算法

经典问题

  • 主串:adbadbadbf
  • 模式串:adbadf

问题:在主串中查找一个能够匹配模式串的子串。

朴素解法(BF算法)

解决字符串匹配问题,最先想到的是用暴力匹配法BF。

具体做法:在遍历过程中,当遇到字符不匹配时,主串指针回溯到上一次起始位置的后一位,模式串指针归零,从头开始匹配。

时间复杂度O(mn)(m 为主串长度,n 为模式串长度)。

改进思路

每次匹配失败,其实前面已经成功比对了一段字符。暴力算法没有利用这部分信息,导致了重复比较。而我们可以利用这部分的工作来减少匹配次数,而模式串在主串上的移动可以看作是从前缀移动到后缀,因此可以从这一点出发。

BF的例子

下面用一个简化的例子来说明 BF 的不足。

第i轮:

在这里插入图片描述

第i+1轮:

在这里插入图片描述

第j轮:
在这里插入图片描述### 分析

分析第j轮我们发现,模式串中的AB相等。
在这里插入图片描述
由于主串和匹配串匹配时模式串中的 A 与主串中的 C 也相等。也就是说,已经比对过的部分不需要再重新匹配,我们可以直接让模式串的前缀与主串的后缀对齐。

简而言之,当匹配失败时,不必像 BF 那样整体右移一位,而是利用前缀信息,把模式串移动到合适的位置,使前缀和后缀对齐。

基本概念

  • 前缀:必包含首字但不包含尾字。如题包含a、ad、adb、adba、adbad。

  • 后缀:不包含首字但必包含尾字。如题包含f、df、adf、badf、dbadf。

  • 最长相等前后缀:某个子串中,前缀和后缀相等的最大长度字符串。

next 数组(前缀表)

定义与作用

  • 定义(长度版):next[j]是以下标 j 结尾子串的最长相等前后缀的长度/前缀后续的第 一位字符的位置。

  • 作用:当主串和模式串中某一字符不匹配时,告诉模式串应该回退到哪个位置继续匹配。求前缀表的过程实质是一个动态规划的过程。

示例推导

根据题目中的模式串为例:

子串aadadbadbaadbad
最长相等前后缀长度01012

next = [0, 1, 0, 1, 2]

快速计算next的代码

void getNext(int* next, const string& s){
    int j=0; // 前缀末尾字符/当前最长相等前后缀的长度 - 前缀0~j
    next[0]=0; // 若0处不匹配则回退到0 || 同时这步也意味着跳过"a"
    for(int i=1;i<s.size();++i){ // 待匹配的后缀末尾字符 || 此处1是因为只有一个字符时不存在前后缀 - 后缀1~i
		while(j>0&&s[i]!=s[j]){ // 前后缀不相等
			j=next[j-1];
        }
        if(s[i]==s[j]) ++j;// 前后缀相等
        next[i]=j; // 更新当前字符的next
    }
}

流程:初始化→ 遇到不等 → 回退 j → 匹配成功 → 更新 next

如何回退(核心逻辑)

在求解 next 数组时,最关键的一步就是 回退 j

while (j > 0 && s[i] != s[j]) {
    j = next[j - 1];
}
  • 含义:如果当前字符不匹配,就不断缩短“前缀”的长度,尝试更短的前缀是否能继续匹配。
  • 为什么是 while? 因为可能需要多次回退,直到找到可以匹配的位置,或者回退到 0 为止。
图示回退
  1. 不需要回退(直接匹配成功)

    此时s[i]==s[j],说明当前字符能直接延长已有前缀,此时只需 j++
    在这里插入图片描述
    👉 **这是最理想情况:**next[i] = next[i-1] + 1。

  2. 回退后立即成功

    s[i] != s[j] 时,进入 while 循环:

    初始比较:s[i] != s[j],触发一次回退:j = next[j-1]

    回退后重新比较,发现 s[i] == s[j],此时匹配成功,更新 next[i]
    在这里插入图片描述
    👉 这是回退一次后匹配成功的情况:next[i] = next[next[i-1]] + 1。

  3. 回退两次

    s[i] != s[j] 时,进入 while 循环:

    第一次比较s[i] != s[j],触发一次回退:j = next[j-1]

    第一次比较s[i] != s[j],再次触发一次回退:j = next[j-1]
    在这里插入图片描述回退后重新比较,发现 s[i] == s[j],此时匹配成功,更新 next[i]
    在这里插入图片描述
    👉 这是 多次回退 的情况:next[i] = next[next[next[i-1]]] + 1。

    • 如果最终找到相等字符,则匹配成功。
    • 如果退到 0 仍不相等,则 next[i] = 0

为什么连续多次回退是对的呢?

设模式串为 P,文本串为 S,当前比较位置 iSjP。假设已有匹配关系:

P[0..j-1] == S[i-j..i-1]

现在比较 S[i]P[j]

  • 如果相等,则继续扩展;
  • 如果不等,根据 next[j-1] 的定义,存在长度 k = next[j-1] 的前缀满足
P[0..k-1] == P[j-k..j-1]

于是可以令 j = k,相当于将模式串前缀 P[0..k-1] 与文本串后缀对齐,再与 S[i] 进行比较。

如果仍不匹配,就继续递归地取 next[k-1]。因为 next 记录了“最长、次长、再次长…”的相等前后缀关系,所以多次回退依次检查所有可能的候选位置,直到找到合适的匹配,或回退到 0 为止。

因此,连续多次回退不会漏掉任何可能的匹配点,也不会做无效比较

KMP使用next数组来匹配

核心思想

这个过程和求 next 数组时的思路是类似的,都是在失败时不断回退 j,直到找到合适的前后缀对齐位置。

匹配过程中维护两个下标:

  • i 指向主串 s
  • j 指向模式串 t

流程

  1. 如果 s[i] == t[j],则说明当前字符匹配成功,同时移动 ij
  2. 如果 s[i] != t[j],则通过 next 数组回退 j,让模式串前缀与已匹配的后缀对齐,从而避免重复比较;
  3. j 移动到模式串末尾,说明找到了完整匹配,此时返回匹配的起始位置。

👉 由于每个字符最多被比较两次(一次失败、一次回退后成功),总时间复杂度为 O(m+n)

具体KMP匹配代码

int KMP(const string &s, const string &t) {
    int next[t.size()];
    getNext(t, next);  // 先构造模式串的 next 数组

    int j = 0; // j 表示当前在模式串中的匹配位置
    for (int i = 0; i < s.size(); i++) {
        // 如果当前字符不匹配,则回退 j
        while (j > 0 && s[i] != t[j]) {
            j = next[j - 1];
        }
        
        // 如果匹配成功,j 向前推进
        if (s[i] == t[j]) j++;
        
        // 如果 j 到达模式串末尾,说明完全匹配
        if (j == (int)t.size()) {
            return i - t.size() + 1; // 返回匹配起始位置
        }
    }
    return -1; // 未找到
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值