KMP 算法

字符串的本质就是一个字符数组,串中任意个连续的字符组成的子序列称为该串的子串,包含子串的串相应的称为主串.

子串的定位操作通常被称为串的模式匹配,它求的是子串在主串中的位置. 最简单的方法可以从主串的第一个字符开始匹配,依次迭代,其最坏时间复杂福为 O ( n m ) O(nm) O(nm),其中 n n n m m m 分别是主串和模式串的长度.

在这种暴力匹配中,每趟匹配失败都是模式后移一位再从头开始比较,浪费了大量的时间,KMP 算法改进了这一点,避免了主串指针的回溯.

1. 字符串的前缀、后缀和部分匹配值

  • 前缀:除最后一个字符外,字符串的所有头部子串
  • 后缀:除第一个字符外,字符串的所有尾部子串
  • 部分匹配值:字符串的前缀和后缀的最长相等前后缀长度

"ababa" 为例:

  • a 的前缀和后缀都为空集,最长相等前后缀长度为 0 0 0
  • ab 的前缀为 {a},后缀为 {b} a ∩ b = ∅ {a}\cap{b}=\varnothing ab=,最长相等前后缀长度为 0 0 0
  • aba 的前缀为 {a,ab},后缀为 {a,ba} a ∩ b = a {a}\cap{b}={a} ab=a,最长相等前后缀长度为 1 1 1
  • abab 的前缀为 {a,ab,aba},后缀为 {b,ab,bab} a ∩ b = a b {a}\cap{b}=ab ab=ab,最长相等前后缀长度为 2 2 2
  • ababa 的前缀为 {a,ab,aba,abab},后缀为 {a,ba,aba,baba} a ∩ b = a b a {a}\cap{b}={aba} ab=aba,最长相等前后缀长度为 3 3 3

故字符串 ababa 的部分匹配值为 001023,以表格的形式展示就是:

编号12345
Sababa
部分匹配值00123

部分匹配值有什么用呢?

考虑下图所示的模式匹配情况:

在这里插入图片描述

模式串在字符 C 处不匹配,如果是暴力匹配,则会从模式串首字符开始匹配,而 KMP 算法是从部分匹配值之后的字符开始匹配.

显然,这里避免了回溯主串,提高了匹配效率.

那么,每次模式串需要移动多少长度呢?

移动位数=已匹配的字符数-对应的部分匹配值长度

2. KMP 算法

从前面可以看出来,KMP 算法的核心就是 2 个问题:

  • 求出模式串的部分匹配值
  • 求出当主串指针 i i i 和模式串指针 j j j 不匹配时,指针 j j j 重新指向的位置(保持 i i i j j j 对应)

在 KMP 算法的描述中,这其实是求 next 数组(注意,下标从 1 1 1 开始,表示第几个字符).

我们规定 next[j] 表示主串第 i i i 个字符和模式串第 j j j 个字符不匹配时,模式串指针 j j j 重新指向的模式串的位置(第几个字符),如上例 next[6]=3.

当模式串第一个字符( j = 1 j=1 j=1 )与主串第 i i i 个字符发生失配时,规定 next[1]=0,表示第一个字符就不匹配时,模式串直接右移,从主串的下一个位置( i + 1 i+1 i+1 ) 和模式串的第一个字符继续比较. 其他部分匹配值长度的情况下, j j j 重新指向模式串第一个字符,即从第一个字符开始比较(至少 next[2]=1).

设模式串为 pnext[1]=0next[j]=k,此时 next[j+1] 可能有两种情况

  • p[k]=p[j]

    在这里插入图片描述

    此时 next[j+1]=next[j]+1

  • p[k]!=p[j]

    在这里插入图片描述

    此时按照上面的方法行不通了,怎么办了,继续对序列 p 1 p 2 ⋯ p k − 1 p_1p_2\cdots p_{k-1} p1p2pk1 执行同样的操作,直到与 p j p_j pj 匹配或者递归到终止条件

至此,我们可以得出获取 next 数组的代码:

// Java 
public int[] getNext(@NotNull final char[] p) {
    int[] next = new int[p.length];
    int j = 0, k = -1;
    next[j] = k;
    while (j < p.length - 1) {
        if (k == -1 || p[j] == p[k]) {
            ++j;
            ++k;
            next[j] = k;
        } else {
            k = next[k];
        }
    }
    return next;
}

注意,之前为了好理解,设起始下标为 1 1 1,编程实现中数组一般从下标 0 0 0 开始,故之前的位置对应要减 1 1 1.

下面就可以开始看看 KMP 的整体逻辑了:

// Java
public int kmp(@NotNull final char[] s, final @NotNull char[] p, final @NotNull int[] next) {
    assert p.length > 0 && s.length > p.length;
    int i = 0, j = 0;
    while (i < s.length && j < p.length) {
        if (j == -1 || s[i] == p[j]) {
            ++i;
            ++j;
        } else {
            j = next[j];
        }
    }
    if (j >= p.length) {
        return i - p.length;
    } else {
        return -1;
    }
}

3. KMP算法的进一步优化

优化想法很简单,next 数组还需要递归调来得到发生不匹配时模式串移动到的位置,我们可不可以直接一次性在数组中给出,而不是在调用时反复递归判断?因此我们可以只求出 nextVal 数组.

// Java
public int[] getNextVal(@NotNull final char[] p) {
    int[] nextVal = new int[p.length];
    int j = 0, k = -1;
    nextVal[j] = k;
    while (j < p.length - 1) {
        if (k == -1 || p[j] == p[k]) {
            ++j;
            ++k;
            if (p[j] != p[k]) {
                nextVal[j] = k;
            } else {
                nextVal[j] = nextVal[k];  // 为啥呢?因为如果相等了,那和之前比较的就一样了,再比较一次毫无意义
            }
        } else {
            k = nextVal[k];
        }
    }
    return nextVal;
}

PS:有没有感觉求 next 数组和 nextVal 数组使用的都是动态规划的思想.

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值