基础算法学习:kmp

基础算法学习:kmp

KMP(Knuth-Morris-Pratt)算法是一种用于字符串匹配的高效算法,它可以在给定的文本中查找一个模式串(子串)的所有出现位置。与暴力算法不同,KMP算法通过预处理模式串来提高匹配效率,避免了重复的字符比较。

KMP算法的基本思想:

KMP算法的核心思想是利用已经部分匹配的信息来减少无效的比较。通过一个辅助数组(称为部分匹配表前缀函数)来记录模式串中每个位置前后缀的相似度,这样当遇到不匹配时,模式串可以跳过某些字符,避免从头开始匹配。

主要步骤:

  1. 构建前缀函数(Partial Match Table) :前缀函数用于存储模式串每个位置的最长前后缀相同的部分的长度。
  2. 模式串匹配过程:使用前缀函数来跳过已经匹配过的部分,减少无效的匹配。

前缀函数:

前缀函数 pi[i]​ 表示模式串从 0​ 到 i​ 位置的子串的最长前后缀相同的长度。前缀是指子串的一个前缀,后缀是指该子串的一个后缀。举个例子,如果模式串是 "ABAB"​,那么它的前缀函数如下:

  • pi[0] = 0​,因为没有前缀。
  • pi[1] = 0​,因为没有匹配的前后缀。
  • pi[2] = 1​,因为 "A"​ 是 "AB"​ 的前后缀。
  • pi[3] = 2​,因为 "AB"​ 是 "ABAB"​ 的前后缀。

KMP算法的过程:

  1. 构建前缀函数数组 pi​。
  2. 进行字符串匹配:用一个指针遍历文本,另一个指针遍历模式串。当两者匹配时,继续向后移动;当不匹配时,根据 pi​ 数组调整模式串的位置,避免重复匹配已知部分。

时间复杂度:

  • 构建前缀函数的时间复杂度是 O(m),其中 m​ 是模式串的长度。
  • 匹配过程的时间复杂度是 O(n),其中 n​ 是文本的长度。 因此,KMP算法的总时间复杂度是 O(m + n),比暴力算法的 O(m * n) 要高效得多。

KMP详解

给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模式串 P在字符串 S中多次作为子串出现。

求出模式串 P在字符串 S中所有出现的位置的起始下标。

输入格式

第一行输入整数 N,表示字符串 P 的长度。

第二行输入字符串 P。

第三行输入整数 M,表示字符串 S的长度。

第四行输入字符串 S。

输出格式

共一行,输出所有出现位置的起始下标(下标从 0开始计数),整数之间用空格隔开。

数据范围

1≤N≤1e5

1≤M≤1e6

输入样例:

3

aba

5

ababa

输出样例:

0 2

1.串的普通算法BF

BF算法图示过程(返回匹配成功的位置)

思想:

从主串的第pos个字符开始匹配和模式串中第一个字符串开始比较。

(1)如果相等:继续比后续字符,i++,j++;

(2)如果不相等,从主串的下一个字符和模式串 的第一个字符相比较。

任何求主串的下一个字符的位置?

方法一:设置一个变量k,在主串未开始时,领k=i+1(主串的下一个位置),每当匹配失败,另i=j,即可。

int bf(char s[],char t[],int pos)
{
    int i=pos,j=1;//从主串的第pos个字符,和模式串第一个字符比较
    while(i<=s.length&&j<=t.length)
    {
        int k=i+1; //让k等于i的下一个位置
        if(s[i]==t[j]) //匹配成功,继续比较下一个位置
        {
            ++i;
            ++j;
        }
        else //匹配失败
        {
            i=k;
            j=1;
        }
    }
    if(j>T.length) return i-T.length;//如果j大于模式串的长度,说明匹配成功
    else return 0; //匹配失败
}

方法二:找出每次失败i和j的关系。

则下一个位置是i-j+2.

int BF(char s[],char t[],int pos)
{
    int i=pos,j=1;
    while(i<=s.length&&j<t.length)
    {
        if(s[i]==s[j]) 
        {
            ++i;
            ++j;
        }
        else
        {
            i=i-j+2;
            j=1;
        }
    }
    if(j>t.length) return i-t.length;
    else return 0;
}

2.KMP算法

特点:在匹配过程中,不需要回溯主串的指针i,时间复杂度为O(m+n)

思路:

则我们可知next数组的含义,next[i]表示:以i结尾的后缀和从1开始模式串的前缀相等,且相等最大 。

假设我们已知next数组,则模式匹配如下:

思想

主串的第pos个字符和模式串的第一个字符串进行比较

(1).相等:继续比较后继字符 i++,j++。

(2).不相等:主串的位置不变和模式串的第next[j]字符比较,j=next[j]。

下面展示一个代码:

int KMP(char s[],char t[],int pos)
{
    int i=pos,j=1;
    while(i<=s.length&&j<=t.length)
    {
        if(j==0||s[i]==t[j]) //j==0表示当前比较的是模式串的首字符且不匹配,应从主串的后一个位置继续匹配;s[i]==t[j]表示匹配成功,继续匹配。
        {
            ++i;
            ++j;
        }
        else j=next[j];
    }
    if(j>t.length) return i-t.length;
    else return 0;
}

求KMP的next指针的值

(1)如果t[j]==t[next[j]],则next[j+1]=next[j]+1.

(2)如果t[j]!=t[next[j]],判断t[j]和t[next[…next[j]…]],重复 过程(1),直到相等,退到0时,表示不存在,next[j+1]=1.

换句话说,要求next[j],需要判断t[j-1]和t[next[j-1]].

void get_next(char t[],int next[])
{
    int j=1,k=0;
    next[1]=0;
    while(j<t.length)
    {
        if(k==0||t[j]==t[k])//k为0,或者找到时,next[j+1]=k。
        {
            ++j;
            ++k;
            next[j]=k;
        }
        else k=next[k];
    }
}

KMP的nextval值

思想:

当s[i]和t[j]比较后,发现两者不相等时,但t[j]和t[k]相等,那就意味着s[i]和t[k]不需要进行额外的比较,因此j的位置的nextval值修改为k位置的nextval值,当s[i]和t[j]比较后,发现两者不相等,发现t[j]和t[k]也不相等,因此j位置的nextval值仍是k,即nextval[j]=next[j].

已知next[j],应如下修改nextval值

k=next[j];

if(t[j]==t[k]) nextval[j]=next[k];

else nextval[j]=next[j];

例如:求aaaab的nextval值。

如果t[j]==t[next[j]],nextval[j]=nextval[next[j]]

否则nextval[j]=next[j].

void get_nextval(chat t[],int next[],int nextval[])
{
    int j=2,k=0;
    get_next(t,next);
    nextval[1]=0;
    while(j<=t.length())
    {
        k=next[j];
        if(t[j]==t[k]) nextval[j]=nextval[j];
        else nextval[j]=next[j];
    }
}

匹配过程和next的匹配过程类似。

AC代码

#include <iostream>
using namespace std;

const int N = 100100, M = 1000010;

int n, m;       // n:模式串 p 的长度,m:主串 s 的长度
int ne[N];      // next 数组(又称为部分匹配表),用于记录模式串 p 中每个位置的最长相等前后缀长度
char s[M], p[N]; // s:主串,p:模式串
                  // 注意:这里均采用 1-indexing,即字符串从下标 1 开始存储

// 求模式串 p 的 next 数组,也就是部分匹配表
void get_next() {
    // i 从 2 开始,因为位置 1 的 next 值通常为 0(空串没有前后缀匹配)
    // j 表示当前匹配到的位置(即 p[1...j] 是 p[1...i-1] 的后缀,同时也是前缀)
    for (int i = 2, j = 0; i <= n; i++) {
        // 如果 p[i]与 p[j+1]不匹配,就回退 j 到 ne[j],直到找到合适的 j 或者 j 回退到 0
        while (j && p[i] != p[j + 1])
            j = ne[j];  // 这里利用已经计算好的部分匹配信息,将 j 回退到较小的匹配值
  
        // 如果 p[i]与 p[j+1]匹配,则 j 向前扩展一位
        if (p[i] == p[j + 1])
            j++;
  
        // 将当前位置 i 的 next 值设为 j,即 p[1...j]为 p[1...i] 的最长相等前后缀
        ne[i] = j;
    }
}

// 利用 KMP 算法在主串 s 中查找模式串 p 出现的位置
void kmp() {
    // i:遍历主串 s,j:当前匹配模式串 p 的位置
    for (int i = 1, j = 0; i <= m; i++) {
        // 当 j > 0 且当前字符 s[i] 与 p[j+1]不匹配时,
        // 通过 next 数组将 j 回退到较小的匹配状态(即继续尝试匹配)
        while (j && s[i] != p[j + 1])
            j = ne[j];  // 回退至上一个可能的匹配位置
  
        // 如果 s[i] 与 p[j+1]匹配,则 j 向前扩展一位
        if (s[i] == p[j + 1])
            j++;
  
        // 当 j 达到模式串长度 n 时,说明找到了一个完整匹配
        if (j == n) {
            // 输出匹配位置,注意这里输出的是 i - n,
            // 因为 i 表示匹配结束的位置,i - n 即为匹配起始位置(以 1 为下标时)
            printf("%d ", i - n);
      
            // 继续查找下一个匹配,将 j 回退到上一个可能继续匹配的位置
            j = ne[j];
        }
    }
}

int main() {
    // 输入格式:首先输入模式串长度 n,
    // 接着输入模式串 p(从 p+1 开始存储,即 p[1] 为模式串的第一个字符),
    // 然后输入主串长度 m,接着输入主串 s(同样从 s+1 开始存储)。
    cin >> n >> (p + 1) >> m >> (s + 1);
  
    // 预处理模式串,求出部分匹配表
    get_next();
  
    // 执行 KMP 算法,查找模式串在主串中所有的出现位置
    kmp();
  
    return 0;
}


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值