字符串匹配 - KMP算法

2020-03-30  本文已影响0人  天命_风流

前面我们介绍非常高效的 BM 算法,今天我们介绍另一个非常出名且高效的 KMP 算法。

KMP 算法思想

KMP 算法的思想和 BM 算法类似,都是希望可以在匹配失败之后多跳跃几次,避免不必要的匹配。
在 BM 算法中,我们使用了 坏字符 和 好后缀规则,在 KMP 算法中,我们使用好前缀规则实现跳跃:

KMP-好前缀规则.jpg
那么,好前缀该如何确定滑动范围呢?
首先我们要找到好前缀,在好前缀的后缀子串中,查找最长的那个可以跟好前缀的前缀子串匹配的。假设最长的可匹配的那部分前缀子串是 {v} ,长度是 k 。我们就可以把模式串一次性向后滑动 j-k 位。移动后继续比较。 KMP-滑动位数.jpg
为了表述方便,我把最长可匹配前缀子串的那个后缀子串,叫做最长可匹配后缀子串;对应的前缀子串,叫做最长可匹配前缀子串 KMP-前后缀子串.jpg

你可能发现了,匹配的跳跃规则和主串无关,所以在模式串和主串进行匹配之前,我们可以预先对模式串进行预处理

在预处理过程中,需要一个数组保存最长可匹配前缀子串的结尾字符下标。我们把这个数组定义为 next数组,或者你可以称之为 失效函数

KMP-next数组.jpg
有了 这个数组,我们可以很容易地实现 KMP 算法。这里先假设 next 数组已经计算完成,给出 KMP 算法的框架代码:
// a, b分别是主串和模式串;n, m分别是主串和模式串的长度。
public static int kmp(char[] a, int n, char[] b, int m) {
  int[] next = getNexts(b, m);
  int j = 0;
  for (int i = 0; i < n; ++i) {
    while (j > 0 && a[i] != b[j]) { // 一直找到a[i]和b[j]
      j = next[j - 1] + 1;
    }
    if (a[i] == b[j]) {
      ++j;
    }
    if (j == m) { // 找到匹配模式串的了
      return i - m + 1;
    }
  }
  return -1;
}

next 数组的计算方法

next 的数组可以用笨办法得到,比如要计算下面这个模式串 b 的 next[4],我们把 b[0,4] 的所有后缀子串依次对比: KMP-暴力求next数组.jpg

这样的效率很低,是否有更加高效的方法呢?
答案是肯定的,我们可以利用已经计算出来的 next 值,快速推导出后面的 next 值。

1.叠加相加

如果 next[i-1]=k-1,也就是说,子串 b[0, k-1]是 b[0, i-1]的最长可匹配前缀子串。如果子串 b[0, k-1]的下一个字符 b[k],与 b[0, i-1]的下一个字符 b[i]匹配,那子串 b[0, k]就是 b[0, i]的最长可匹配前缀子串。所以,next[i]等于 k。 KMP-求next-递增.jpg

2.无法叠加,收缩范围

如果新加入的字符无法像之前那样方便地直接相加,那我们可以通过收缩范围看一看它能否让一个稍小的前后缀重叠: KMP-求next-回缩.jpg

解释一下这个图:x 代表后缀子串逐渐向后方收缩,相应的,前缀子串也会向前收缩,当某个时刻 b[i] == b[i-x] 的时候,说明 next 可以在 b[x] ~ b[i-1] 的子串上进行叠加。

3.收缩范围的等价问题

我们知道,只有在后缀子串和前缀子串相同的时候,才可以进行叠加操作。所以,在收缩范围的时候,我们可以将后缀子串的收缩看作前缀子串的收缩。同时,一个子串是否是可匹配前缀子串,我们可以通过之前已经求得的 next 数组很容易地计算出来。

KMP-求next-回缩简化.jpg

next 的构建:
1.如果 b[0,i-1] 的最长前缀与下一个字符 b[i] 相等,则 next[i] = next[i-1] +1
2.如果不相等,我们需要收缩查找范围。其中,后缀向右侧收缩 等价于 前缀向左侧收缩,所以我们可以将问题简化成为前缀收缩。
3.收缩的前缀子串是否是可匹配前缀子串呢?我们可以借助之前的 next 数组快速判断。

代码如下:

// b表示模式串,m表示模式串的长度
private static int[] getNexts(char[] b, int m) {
  int[] next = new int[m];
  next[0] = -1;
  int k = -1;
  for (int i = 1; i < m; ++i) {
    while (k != -1 && b[k + 1] != b[i]) {
      k = next[k];
    }
    if (b[k + 1] == b[i]) {
      ++k;
    }
    next[i] = k;
  }
  return next;
}

性能分析

代码实现

根据上面的思想,我自己实现了 KMP 算法,可以用于参考:

def kmp(a, b):
    '''
    实现KMP算法
    :param a: 主串
    :param b: 模式串
    :return: 位置列表
    '''

    res = []

    n = _next(b)  # 构建模式串的 next 数组

    for i in range(len(a) - len(b) + 1):  # i -> 将作为和 b 对比的基础下标,即 a[i] 将和 b[0] 比较。
        j = 0  # j-> 作为模式串的比对进度的下标
        x = 0  # x-> 作为主串的比对进度的下标
        while j < len(b):  # 没有比对完全
            if a[i + x] == b[j]:  # 如果当前字符串相同,就比对下一个字符
                x += 1
                j += 1
            else:  # 如果字符串不同,我们根据好前缀规则进行跳跃
                k = n[j]  # k->最大可匹配前缀的最后一个字符的下标
                move = j - k  # move->跳跃步数
                i += move  # 跳跃
                break

            if j == len(b):  # 匹配完成,执行下面的语句后就会跳出循环
                res.append(i)

    return res

def _next(b):
    '''
    构建 next 数组
    :param b: 模式串
    :return: next 数组
    '''
    n = [-1 for i in range(len(b))]  # 初始化
    k = -1
    for i in range(1, len(b)):  # 遍历b
        while k != -1 and b[k + 1] != b[i]:  # 当下一个字符和可匹配前缀的下一个字符不符,进行收缩
            k = n[k]
        if b[k + 1] == b[i]:  # 当字符匹配,k + 1
            k += 1
        n[i] = k  # 记录

    print(n)
    return n


if __name__ == '__main__':
    a = 'aacbccaaabcaa'
    b = 'aaabcaa'
    r = kmp(a, b)
    print(r)

以上就是 KMP 算法的全部内容

注:本文章的主要内容来自我对极客时间app的《数据结构与算法之美》专栏的总结,我大量引用了其中的代码和截图,如果想要了解具体内容,可以前往极客时间

上一篇下一篇

猜你喜欢

热点阅读