数据结构基础4:KMP

KMP

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

暴力做法:

//S长度为n,P长度为m
for (int i = 1; i <= n; i ++ )
{
    //寻找原串
    bool flag = true;
    for (int j = 1; j <= m; j ++ )
    {
        if (s[i + j - 1] != p[j])
        {
            flag=false;
            break;
        }
    }
}

利用字符串的特殊信息来减少暴力枚举的次数

当我们匹配了一段失败后,会将模式串向后移动以希望能够继续匹配上

那么就存在第二次开始匹配能成功的一段字符串的前缀等于上一次匹配能成功的一段字符串的后缀

对于每一个点预处理出来一段后缀,要最长的一段后缀和前缀相等

字符串的某一段前后缀相等

n e x t next next记录的就是当前作为后缀末位的 j j j对应的前缀末位的位置

n e x t [ i ] = j next[i] = j next[i]=j

p [ 1 , j ] = p [ i − j + 1 , i ] p[1,j] = p[i - j + 1, i] p[1,j]=p[ij+1,i]

S = "abababc"
P = "abababab"
     12345678
// 长度为i的前缀的border
//P的next数组
ne[1] = 0;	//a
ne[2] = 0;	//ab
ne[3] = 1;	//aba
ne[4] = 2;	//abab
ne[5] = 3;	//ababa
ne[6] = 4;	//ababab
ne[7] = 5;	//abababa
ne[8] = 6;	//abababab

KMP算法模板:

O ( n ) O(n) O(n)

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
//在某个头文件里面有next,所以一般用ne作为数组名
//求模式串的Next数组:

string p, s;
int ne[N];

cin >> n >> p >> m >> s;
p = " " + p, s = " " + s;

for (int i = 2, j = 0; i <= n; i ++ )
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    // 对于前一个border,我们往后延长一位,如果p[i] == p[j + 1],就可以得到长度+1的border
    // 如果不行,就去前缀的前缀判断
    if (p[i] == p[j + 1]) j ++ ;
    ne[i] = j;
}

// 匹配
for (int i = 1, j = 0; i <= m; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == n)
    {
        // 匹配成功,输出匹配位置下标(此处从0开始计数,如果要从1开始就是i - n + 1)
        printf("%d ", i - n);
        j = ne[j];
    }
}

全新的理解方式:

前置知识

字符串匹配问题中,给出两个字符串 text 和 pattern (本题中 text 为 S, pattern 为 P),需要判断 pattern 是否是 text 的子串。一般把 text 称为文本串,pattern 称为模式串。暴力的解法为:
枚举文本串 text 的起始位置 i ,然后从该位开始逐位与模式串 pattern 进行匹配。如果匹配过程中每一位都相同,则匹配成功;否则,只要出现某位不同,就让文本串 text 的起始位置变为 i + 1,并从头开始模式串 pattern 的匹配。假设 m 为文本串的长度,n 为模式串的长度。时间复杂度为 O(nm)。显然,当 n 和 m 都达到 1 0 5 10^5 105 级别时无法承受。

next直观理解

假设有一个字符串 s (下标从 0 开始),那么它以 i 号位作为结尾的子串就是 s[0…i]。对该子串来说,长度为 k + 1 的前缀与后缀分别是 s[0…k] 与 s[i-k…i]。我们构造一个 int 型数组(叫啥无所谓,就叫它 next吧)。其中,next[i] 表示使字串 s[0…i] 中前缀 s[0…k] 等于后缀 s[i-k…i] 的最大的 k。(注意相等的前缀、后缀在原字串中不能是 s[0…i] 本身。这点很重要,在后面感性分析时会用到);如果找不到相等的前后缀,就令 next[i] = -1。显然,next[i] 就是所求最长相等前后缀中前缀的最后一位的下标。

第一种方法直接画线画出子串 s[0…i] 的最长相等前后缀:

在这里插入图片描述

第二种方法在上部给出后缀,下部给出前缀,再将相等的最长前后缀框起来。

在这里插入图片描述

next[i] 就是使子串 s[0…i] 有最长相等前后缀的前缀的最后一位的下标。(这里是从0开始的下标)

我们假设已经求出了 next[0] ~ next[i-1],用它们来推算出 next[i]

还是用我们刚刚感性认识的 s = “abababc” 作为例子。假设已经有了 next[0] = -1、next[1] = -1、next[2] = 0、next[3] = 1,现在来求解 next[4]。如下图所示,当已经得到 next[3] = 1 时,最长相等前后缀为 “ab”,之后计算 next[4] 时,由于 s[4] == s[next[3] + 1] (这里的为什么要用 next[3]?想想至尊概念),因此可以把最长相等前后缀 “ab” 扩展为 “aba”,因此 next[4] = next[3] + 1,并令 j 指向 next[4]。

在这里插入图片描述

接着在此基础上求解 next[5]。如下图所示,当已经得到 next[4] = 2 时,最长相等前后缀为 “aba”,之后计算 next[5] 时,由于 s[5] != s[next[4] + 1],因此不能扩展当前相等前后缀,即不能直接通过 next[4] + 1 的方法得到 next[5]。既然相等前后缀没办法达到那么长,那不妨缩短一点!此时希望找到找到一个 j,使得 s[5] == s[j + 1] 成立,同时使得图中的波浪线 ~,也就是 s[0…j] 是 s[0…2] = “aba” 的后缀,而 s[0…j] 是 s[0…2] 的前缀是显然的。同时为了找到相等前后缀尽可能长,找到这个 j 应尽可能大。

在这里插入图片描述

实际上我们上图要求解的 ~ 部分,即 s[0…j] 既是 s[0…2] = “aba” 的前缀,又是 s[0…2] = “aba” 的后缀,同时又希望其长度尽可能长,那么 s[0…j] 就是 s[0…2] 的最长相等前后缀。也就是说,只需要令 j = next[2],然后再判断 s[5] == s[j + 1] 是否成立:如果成立,说明 s[0…j + 1] 是 s[0…5] 的最长相等前后缀,令 next[5] = j + 1 即可;如果不成立,就不断让 j = next[j],直到 j 回到了 -1,或是途中 s[5] == s[j + 1] 成立。

在这里插入图片描述

如上图所示,j 从 2 回退到 next[2] = 0,发现 s[5] == s[j + 1] 不成立,就继续让 j 从 0 回退到 next[0] = -1;由于 j 已经回退到了 -1,因此不再继续回退。这时发现 s[i] == s[j + 1] 成立,说明 s[0…j + 1] 是 s[0…5] 的最长相等前后缀,于是令 next[5] = j + 1 = -1 + 1 = 0,并令 j 指向 next[5]。

下面总结 next 数组的求解过程,并给出代码:

  1. 初始化 next 数组,令 j = next[0] = -1。
  2. 让 i 在 1 ~ len - 1范围内遍历,对每个 i ,执行 3、4,以求解 next[i]。
  3. 直到 j 回退为 -1,或是 s[i] == s[j + 1] 成立,否则不断令 j = next[j]。
  4. 如果 s[i] == s[j + 1],则 next[i] = j + 1;否则 next[i] = j。

在此基础上我们进入 kmp,有了上面求 next 数组的基础,kmp 算法就是在照葫芦画瓢,给定一个文本串 text 和一个模式串 pattern,然后判断模式串 pattern 是否是文本串 text 的子串。
以 text = “abababaabc”、pattern = “ababaab” 为例。令 i 指向 text 的当前欲比较位,令 j 指向 pattern 中当前已被匹配的最后位,这样只要 text[i] == pattern[j + 1] 成立,就说明 pattern[j + 1] 也被成功匹配,此时让 i、j 加 1 继续比较,直到 j 达到 m - 1(m 为 pattern 长度) 时说明 pattern 是 text 的子串。在这个例子中,i 指向 text[4]、j 指向 pattern[3],表明 pattern[0…3] 已经全部匹配成功了,此时发现 text[i] == pattern[j + 1] 成立,这说明 pattern[4] 成功匹配,于是令 i、j 加 1。

在这里插入图片描述

接着继续匹配,此时 i 指向 text[5]、j 指向 pattern[4],表明 pattern[0…4] 已经全部匹配成功。于是试着判断 text[i] == pattern[j + 1] 是否成立:如果成立,那么就有 pattern[0…5] 被成功匹配,可以令 i、j 加 1 以继续匹配下一位。但此处 text[5] != pattern[4 + 1],匹配失败。那么我们这里该怎么做?放弃之前 pattern[0…4] 的成功匹配成果,让 j 回退到 -1 开始重新匹配吗?那是暴力解的方法,我们来看一下 kmp 的处理。

为了不让 j 直接回退到 -1,应寻求回退到一个离当前的 j (此时 j 为 4)最近的 j’,使得 text[i] == pattern[j’ + 1] 能够成立,并且 pattern[0…j’] 仍然与 text 的相应位置处于匹配状态,即 pattern[0…j’] 是 pattern[0…j] 的后缀。这很容易令人想到之前的求 next 数组时碰到的类似问题。答案是 pattern[0…j’] 是 pattern[0…j] 的最长相等前后缀。也就是说,只需要不断令 j = next[j],直到 j 回退到 -1 或者是 text[i] == pattern[j + 1] 成立,然后继续匹配即可。next 数组的含义就是当 j + 1 位失配时,j 应该回退到的位置。对于刚刚的例子,当 text[5] 与 pattern[4 + 1] 失配时,令 j = next[4] = 2,然后我们会发现 text[i] == pattern[j + 1] 能够成立,因此就让它继续匹配,直到 j == 6 也匹配成功,这就意味着 pattern 是 text 的子串。

kmpxiugai1.jpg

kmp 算法的一般思路如下:

  1. 初始化 j = -1,表示 pattern 当前已被匹配的最后位。
  2. 让 i 遍历文本串 text,对每个 i,执行 3、4来试图匹配 text[i] 和 pattern[j + 1]。
  3. 直到 j 回退到 -1 或者是 text[i] == pattern[j + 1],否则不断令 j = next[j]。
  4. 如果 text[i] == pattern[j + 1],则令 j ++。如果 j 达到 pattern_len - 1,说明 pattern 是 text 的子串。

我们观察上面的分析,能否发现:求解 next 数组的过程其实就是模式串 pattern 进行自我匹配的过程。

考虑如何统计 pattern 在 text 中出现的起始下标:
当 j = m - 1 时表示 pattern 完全匹配,此时可以输出 i - j (text 的结束位置减去 pattern 的长度就是 pattern 在 text 中出现的下标)。但问题在于:之后应该从 pattern 的哪个位置开始进行下一次匹配? 由于 pattern 在 text 的多次出现可能是重叠的,因此不能什么都不做就让 i 加 1继续进行比较,而是必须先让 j 回退一段距离。此时 next[j] 代表着整个 pattern 的最长相等前后缀,从这个位置开始让 j 最大,即让已经匹配的部分最长,这样能保证既不漏解,又使下一次匹配省去许多无意义的比较。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

钰见梵星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值