C++KMP算法详解

门情提要,参考资料:oi wiki前缀函数与 KMP 算法 - OI Wiki.

资料视频链接:最浅显易懂的 KMP 算法讲解_哔哩哔哩_bilibili.

看完文章了解KMP,直接干碎NOIP!

一、基本概念

        KMP算法(Knuth-Morris-Pratt算法)是一种高效的字符串匹配算法,用于在一个主字符串(文本串)中查找模式串(子串)的出现位置。与暴力匹配算法相比,KMP算法通过预处理模式串,避免了不必要的字符比较,从而将时间复杂度降低到线性级别(O(n + m),其中n是主字符串长度,m是模式串长度)。

二、核心思想

        在字符串匹配中,暴力算法会在每次失配后将模式串右移一位,重新开始匹配,进而导致大量重复计算。而KMP算法则通过预处理字符串来构建一个部分匹配表(Partial Match Table,简称PMT),记录模式串中每个位置之前的子串的最长相等前后缀长度。利用这个表,KMP算法可以在失配时跳过已知的匹配部分,从而提高匹配效率。

        关键点:

                1.最长相等前后缀:对于模式串的一个子串,其最长相等前后缀是指最长的相同前缀和后缀。

                2.部分匹配表(PMT):也称为next数组,记录每个位置的最长相等前后缀长度。

                3.失配时的跳转:当匹配失败时,根据next数组直接跳到下一个可能的匹配位置,而不是逐个字符移动。

三、部分匹配表(PMT)的构建

        部分匹配表为KMP算法的关键。本质为记录模式串中每个位置的最长相等前后缀长度。(其可将KMP的时间复杂度由  O(n * m)  降到  O(n + m))

        其构建过程如下:

                1.初始化    next[0]  =  0  (因为单个字符串没有前后缀) 。

                2.对于每个位置  i ,通过比较当前字符和已知的最长相等前后缀的下一个字符,逐步扩展到最长相等前后缀。

                3.如果匹配成功,则  next[i]  =   next[i-1]  +  1  ;如果匹配失败,则回退到更短的相等前后缀。

ed:

给定一个模式串:"abababca" ;

  i  字符 最长相等前后缀next[i]
  0     a  -     0
  1     b  -     0
  2     a  a     1
  3     b  ab     2
  4     a  aba     1
  5     b  abab     2
  6     c  -     0
  7     a  -     0

以下是匹配表的具体解释:

1) 部分匹配表的作用

在KMP算法中,部分匹配表的作用是在失配时快速跳过已知匹配的部分。具体来说:

  • 当模式串与主字符串匹配失败时,next数组可以告诉我们模式串可以跳过的最大长度。

  • 通过next数组,可以直接跳到下一个可能的匹配位置,而不需要从头开始匹配。

例如,在模式串"abababca"中:

  • 如果匹配到位置i = 5(字符b)时失配,next[5] = 2,表示最长相等前后缀是"ab"

  • 因此,模式串可以直接跳到j = 2的位置继续匹配,跳过了已知匹配的部分"ab"

2) 部分匹配表的计算方法

计算部分匹配表的过程是一个动态规划的过程。以下是计算next数组的步骤:

  1. 初始化

    • next[0] = 0,因为单个字符没有前后缀。

    • j = 0,表示当前已匹配的最长相等前后缀长度。

  2. 遍历模式串

    • 对于每个位置i(从1开始),尝试将当前字符pattern[i]与已匹配的最长相等前后缀的下一个字符pattern[j]进行比较。

    • 如果匹配成功(pattern[i] == pattern[j]),则j++,并设置next[i] = j

    • 如果匹配失败(pattern[i] != pattern[j]),则回退到更短的相等前后缀,即j = next[j - 1],直到找到匹配或j == 0

  3. 更新next数组

    • 每次成功匹配后,更新next[i] = j

以下是计算部分匹配表的代码实现:

cpp复制

vector<int> computePrefixFunction(const string& pattern) {
    int m = pattern.length();
    vector<int> next(m, 0); // 初始化next数组
    int j = 0; // j表示当前已匹配的最长相等前后缀长度

    for (int i = 1; i < m; i++) {
        while (j > 0 && pattern[i] != pattern[j]) {
            j = next[j - 1]; // 失配时回退到更短的相等前后缀
        }
        if (pattern[i] == pattern[j]) {
            j++;
        }
        next[i] = j; // 更新next数组
    }
    return next;
}

3) 部分匹配表的示例

以模式串"abababca"为例,逐步计算next数组:

  1. 初始化

    • next[0] = 0j = 0

  2. 计算next[1]

    • i = 1pattern[1] = 'b'j = 0

    • pattern[1] != pattern[0]j保持为0。

    • next[1] = 0

  3. 计算next[2]

    • i = 2pattern[2] = 'a'j = 0

    • pattern[2] == pattern[0]j++

    • next[2] = 1

  4. 计算next[3]

    • i = 3pattern[3] = 'b'j = 1

    • pattern[3] == pattern[1]j++

    • next[3] = 2

  5. 计算next[4]

    • i = 4pattern[4] = 'a'j = 2

    • pattern[4] != pattern[2]j = next[1] = 0

    • pattern[4] == pattern[0]j++

    • next[4] = 1

  6. 计算next[5]

    • i = 5pattern[5] = 'b'j = 1

    • pattern[5] == pattern[1]j++

    • next[5] = 2

  7. 计算next[6]

    • i = 6pattern[6] = 'c'j = 2

    • pattern[6] != pattern[2]j = next[1] = 0

    • pattern[6] != pattern[0]j保持为0。

    • next[6] = 0

  8. 计算next[7]

    • i = 7pattern[7] = 'a'j = 0

    • pattern[7] == pattern[0]j++

    • next[7] = 1

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

4) 部分匹配表的直观理解

部分匹配表的核心是利用模式串的自相似性。通过记录每个位置的最长相等前后缀长度,KMP算法可以在失配时跳过已知匹配的部分,而不是从头开始匹配。

例如:

  • 模式串"abab"next数组为[0, 0, 1, 2]

  • 如果匹配到i = 3时失配,next[3] = 2,表示最长相等前后缀是"ab",可以直接跳到j = 2的位置继续匹配。

四、KMP算法的具体实现

        1.构建部分匹配表

vector<int> computePrefixFunction(const string& pattern) {
    int m = pattern.length();
    vector<int> next(m, 0); // next数组
    int j = 0; // j表示当前已匹配的最长相等前后缀长度

    for (int i = 1; i < m; i++) {
        while (j > 0 && pattern[i] != pattern[j]) {
            j = next[j - 1]; // 回退到更短的相等前后缀
        }
        if (pattern[i] == pattern[j]) {
            j++;
        }
        next[i] = j;
    }
    return next;
}

        2.字符串匹配过程

vector<int> kmpSearch(const string& text, const string& pattern) {
    int n = text.length();
    int m = pattern.length();
    vector<int> next = computePrefixFunction(pattern);
    vector<int> matches; // 存储匹配位置

    int j = 0; // j表示当前匹配到模式串的位置
    for (int i = 0; i < n; i++) {
        while (j > 0 && text[i] != pattern[j]) {
            j = next[j - 1]; // 失配时,利用next数组跳过已知匹配部分
        }
        if (text[i] == pattern[j]) {
            j++;
        }
        if (j == m) { // 匹配成功
            matches.push_back(i - m + 1); // 记录匹配位置
            j = next[j - 1]; // 继续查找下一个匹配
        }
    }
    return matches;
}

        3.完整代码示例

#include <iostream>
#include <vector>
#include <string>
using namespace std;

vector<int> computePrefixFunction(const string& pattern) {
    int m = pattern.length();
    vector<int> next(m, 0);
    int j = 0;

    for (int i = 1; i < m; i++) {
        while (j > 0 && pattern[i] != pattern[j]) {
            j = next[j - 1];
        }
        if (pattern[i] == pattern[j]) {
            j++;
        }
        next[i] = j;
    }
    return next;
}

vector<int> kmpSearch(const string& text, const string& pattern) {
    int n = text.length();
    int m = pattern.length();
    vector<int> next = computePrefixFunction(pattern);
    vector<int> matches;

    int j = 0;
    for (int i = 0; i < n; i++) {
        while (j > 0 && text[i] != pattern[j]) {
            j = next[j - 1];
        }
        if (text[i] == pattern[j]) {
            j++;
        }
        if (j == m) {
            matches.push_back(i - m + 1);
            j = next[j - 1];
        }
    }
    return matches;
}

int main() {
    string text = "abababca";
    string pattern = "abab";
    vector<int> matches = kmpSearch(text, pattern);

    if (matches.empty()) {
        cout << "Pattern not found" << endl;
    } else {
        cout << "Pattern found at positions: ";
        for (int pos : matches) {
            cout << pos << " ";
        }
        cout << endl;
    }
    return 0;
}

五、KMP算法的关键点

        1.部分匹配表的作用

                ° 部分匹配表记录了模式串中每个位置的最长相等前后缀长度。

                ° 在失配时,利用next数组可以快速跳过已知匹配部分,避免重复比较。

        2.失配时的跳转

                ° 当text[i] != pattern[j]时,j = next[j - 1],即回退到更短的相等前后缀。

                ° 如果j == 0时仍然失配,则模式串向右移动一 位。

        3.匹配成功的处理

                ° 当j == m时,表示模式串完全匹配,记录匹配位置。

                ° 匹配成功后,j = next[j - 1],继续查找下一个匹配。

六、KMP算法时间复杂度

        1.构建部分匹配表:O(m),其中m是模式串的长度。

        2.字符串匹配过程:O(n),其中n是主字符串的长度。

        3.总时间复杂度:O(n + m),线性级别。

        当然我们并不排除某些题中的询问或者其他操作,KMP的时间复杂度有可能是O((n + x) m)。

七、注意事项

        1.部分匹配表的边界条件:

                ° 当j == 0且失配时,模式串直接向右移动一位。

                ° 部分匹配表的构建需要特别注意边界条件,避免数组越界。

        2.模式串为空:

                ° 如果模式串为空,需要特殊处理,避免逻辑错误。

        3.多模式匹配:

                ° KMP算法主要用于单模式匹配。如果需要匹配多个模式串,可以使用Aho-Corasick算法等扩展方法。

八、总结

        KMP算法是一种高效的字符串匹配算法,通过预处理模式串构建部分匹配表,避免了暴力匹配中的重复计算。它的时间复杂度为线性级别(O(n + m))(不排除特殊的 O((n + x) m)  情况),适用于大规模文本匹配场景。理解KMP算法的关键在于掌握部分匹配表的构建和失配时的跳转机制。

课下习题:P3375 【模板】KMP - 洛谷

                  P5829 【模板】失配树 - 洛谷

                  P2375 [NOI2014] 动物园 - 洛谷

Code ED:

//KMP
//时间复杂度:O(n)
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#define ll long long 
using namespace std;
const int N = 1e6+10;

ll kmp[N];
ll len_a,len_b;//定义a,b两字符串长度 
ll ace;
char a[N],b[N];//a,b字符串 

int main(void){
	scanf("%s", a+1);
	scanf("%s", b+1);
	len_a = strlen(a+1);
	len_b = strlen(b+1);
	//优先处理b数组,之后映射到a数组 
	for(int i = 2; i <= len_b; ++i){
		while(ace && b[i]!=b[ace+1])//匹配失败,会跳,直到成功匹配 
		  ace = kmp[ace];
		if(b[ace+1] == b[i])  ace++;
		  kmp[i] = ace;
	}
	ace = 0;
	//处理a数组,替换替换部分代码就好 
	for(int i = 1; i <= len_a; ++i){
		while(ace > 0  &&  b[ace+1] != a[i])
		  ace = kmp[ace];
		if(b[ace+1] == a[i])
		  ace++;
		if(ace == len_b) {cout << i-len_b+1 << endl; ace = kmp[ace];}//匹配成功,输出 
	}
	for(int i = 1; i <= len_b; ++i)  cout << kmp[i] << " ";
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值