字符串匹配——KMP算法

前段时间被问了字符串匹配的问题,用的是暴力,一直想填KMP的坑,这篇文章就记录一下,方便你我他,希望能讲清楚。

概念

本篇文章的一些约束说明,可能与其他文章不一样,但是原理一样的~

  • 前缀:对于一个字符串s,从第一个字符开始的子串。

  • 后缀:对于一个字符串s,以最后一个字符结尾的子串。

  • 最长相同前后缀 :对于一个字符串s,其前缀子串与后缀子串相同,且长度最大的子串。比如字符串ababaaba,有aaba都是其相同前后缀,而aba是其最长相同前后缀

  • 最长相同前后缀数组:对于一个字符串s,其所有从索引0开始的子串最长相同前后缀长度组成的数组,本文统称为L数组。比如字符串ababaaba,其数组为L = {0, 0, 1, 2, 3, 1, 2, 3}L[i]表示从0i的子串的最长相同前后缀长度,有下表:

    子串最长相同前后缀长度
    a0
    ab0
    aba1
    abab2
    ababa3
    ababaa1
    ababaab2
    ababaaba3

L数组应用

这里我们先说L数组,也就是最长相同前后缀数组有什么用吧,然后再说怎么获得一个字符串的L数组。

普通匹配

这里先举个普通匹配的例子吧:从字符串A = ababaabbababaaba中查找B = ababaaba

Step0
|ababaab|bababaaba
|ababaab|a

Step1
a|babaabbababaaba
 |ababaaba

Step2
ab|aba|abbababaaba
  |aba|baaba

Step3
aba|baabbababaaba
   |ababaaba

Step4
abab|a|abbababaaba
    |a|babaaba

Step5
ababa|ab|bababaaba
     |ab|abaaba

Step6
ababaa|bbababaaba
      |ababaaba

Step7
ababaab|bababaaba
       |ababaaba

Step8
ababaabb|ababaaba|
        |ababaaba|

普通匹配就是一个字符一个字符比较,前面的匹配结果对后面丝毫没有影响,这是个极大的浪费。我们在Step0的匹配都快成功了,只差一个字符,然后又从A的下一个字符重新开始匹配了。而用KMP算法就不一样了。

KMP匹配

同样的例子:从字符串A = ababaabbababaaba中查找B = ababaaba

Step0
|ababaab|bababaaba
|ababaab|a

Step5
ababa|ab|bababaaba
     |ab|abaaba

Step7
ababaab|bababaaba
       |ababaaba

Step8
ababaabb|ababaaba|
        |ababaaba|

使用KMP算法只需要上面几步。为什么KMP可以直接从Step0跳到Step5,再跳到Step7呢?因为KMP算法充分利用了Step0时候匹配到的结果。

  1. Step0匹配结束时的状态为:A字符串的索引为i=7B字符串的索引为j=7,匹配到的子串是ababaab
  2. 对于字符串B = ababaaba,其L数组前面有说过,是L = {0, 0, 1, 2, 1, 1, 2, 3},则其子串ababaab最长相同前后缀长度L[j-1] = 2
  3. 由于A[i]B[j]的字符不匹配,而L[j-1]=2,则将j赋值为L[j-1],此时的状态为:A字符串的索引为i=7B字符串的索引为j=2,匹配到的子串是ab,即Step5结束时的状态。
  4. 而后同理从Step5跳到Step7

这就是L数组,也就是最长相同前后缀数组的作用。通过它,我们可以在匹配失败时,通过L数组,查询与后缀相同的前缀所处的位置,直接重置j的值。

KMP算法代码

L数组的求值

字符串s最长相同前后缀数组L求值如下:

vector<int> getL(string s) {
    unsigned long len = s.size();
    vector<int> L(len, -1);

    for (int i = 1; i < len; i++) {
    	// 对于子串[0,i-1],第i个字符
        int j = L[i - 1];
        // 若子串[0,i-1]存在相同前后缀,找到前缀的最后一个字符的索引
        while ((j >= 0) && (s[j + 1] != s[i])) j = L[j];
        // 判断能否扩展相同前后缀
        if (s[j + 1] == s[i]) L[i] = j + 1;
        else L[i] = -1;
    }

    return L;
}

初始化为-1,表示不存在最长相同前后缀,也是为了方便判断第0位与第i位。
而当L[i]的值不为-1时,表示的是若子串substr(0, i+1)存在相同前后缀子串时,前缀子串的最后一个字符在字符串s中的索引。
那么在KMP算法中,其长度其实是L[i]+1

KMP匹配

字符串的kmp匹配算法,就是在普通匹配的基础上,将j的遍历优化了,其优化原理与求L数组原理一样。
只不过要注意:子串[0, j-1]的最长相同前后缀长度是L[j-1]+1

// 返回第一个匹配到的子串的首字符索引,若没有则返回-1
int kmp(string A, string B) {
    int lenA = A.size(), lenB = B.size();
    assert(lenA > lenB);
    vector<int> L = getL(B);
    int i = 0, j = 0;
    while (i < lenA) {
        if (A[i] == B[j]) {
            i++; j++;
            if (j == lenB) return i - lenB;
        } else {
            if (j == 0) i++;
            // 如果A[i]与B[j]不相等,则将j赋值为子串[0,j-1]的最长前后缀的前缀长度
            else j = L[j - 1] + 1;
        }
    }

    return -1;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法是一种字符串匹配算法,用于在一个文本S内查找一个模式P的出现位置。它的时间复杂度为O(n+m),其n为文本的长度,m为模式的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本为S="ababababca",模式为P="abababca",我们想要在S查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值