KMP算法详解

说道KMP算法,一定要先指出问题所在之处了,问题如下:

已知一个字符串a,b
想找到在字符串a中b首次完整出现的位置。

听到这个问题,首先想到的就是朴素算法,挨个来进行匹配,然后找到对应的答案。

朴素算法

朴素算法很简单,就是挨着遍历,但是会有很多的重复计算,算法复杂度为O(mn)
代码如下:

# 朴素算法
def algorithm1(src, target):
    for i in range(len(src)):
        flag = True
        for j in range(len(target)):
            if i + j >= len(src):
                return -1

            if src[i + j] != target[j]:
                flag = False
                break

        if flag:
            return i

    return -1

考虑如下字符串,重复的计算就很多了,因为直到最后才最终匹配。

src:abbacabbacabbacd
target:abbacd

如果匹配如上两个字符串,就会出现大量的重复计算,所以才有了之后的优化算法

KMP算法

KMP算法跟朴素算法是对朴素算法的一个优化,朴素算法很简单,但是会有大量的重复匹配,而KMP算法就是要把重复的匹配去掉。
KMP算法有个核心的概念,就是next数组,该数组记录最长共有元素的长度。
以abcab为例,其next = [0,0,0,1,2]
说起来比较难以理解,就是部分匹配的值。

算法思想

部分匹配的实质就是其实也是KMP算法的核心思想,KMP算法的核心思想在我看来就是减少匹配的次数,将重复的匹配次数去掉,那这个信息该如何去掉呢?
我们使用两个标志位,一个为主字符串的标志位,一个是模式串的标志位。

代码如下:

# KMP 算法
def algorithm2(src, target):
    l = make_next(target)
    target_length = len(target)
    src_length = len(src)
    src_index = 0
    target_index = 0
    while src_index < src_length:
        while target_index < target_length:
            if src_index + target_index >= src_length:
                return -1
            if src[src_index + target_index] == target[target_index]:
                target_index += 1
            elif target_index >= 1:
                src_index += target_index - l[target_index - 1]
                target_index = l[target_index - 1]
            else:
                src_index += 1
                target_index = 0

        if target_index == target_length:
            return src_index

    return -1

先不看next数组的计算方法,直观的来看KMP算法的话,我们就能理解其思想了,KMP算法虽然是2层循环叠加,但是本质上,src_index 和 target_index 都是只进行了单次遍历的,所以算法复杂度为O(m + n)
比朴素算法的O(mn)快了很多。

了解了算法思想就该了解next数组的计算原理了

next数组

看如下代码

# next 数组的计算
def make_next(target):
    l = [0] * len(target)
    flag = 0
    for i in range(1, len(target)):
        if target[i] == target[flag]:
            flag += 1
        else:
            flag = 0
        l[i] = flag

    return l

看代码可能不慎明了,其实这是计算相同的前后缀来的。
以字符串abcab为例,它的next数组为[0,0,0,1,2],
其前三个字符没有公共的前后缀,所以都是0,到了第四位,模式串有了一个公共的前后缀,就是a然后其字符长度1,所以next[3] = 1
到了第五位,其公共的前后缀变成了ab,所以next[4] = 2
详细列如下:

next值 模式串 前缀 后缀 公共部分
2 abcab abca,abc,ab,a bcab,cab,ab,a ab
1 abca abc,ab,a bca,ca,a a
0 abc ab,a bc,c

后面的就不用说了,所有的公共前后缀均为无

而公共的前后缀可以用来计算下次的src_index的偏移距离,如果模式串在匹配到src_index位置时不匹配了,可以根据之前src_index 的下次偏移的位置应该就是src_index += target_index - l[target_index - 1]
增量为模式串已经匹配的长度与next数组的在该处值的差

其实仔细想想就可以理解该处的意义,因为l[target_index - 1]其实代表的是之前最后匹配处的公共匹配值,如果没有匹配,那么偏移的距离则正好应该为target_index

说的比较复杂,实际上就是有2个问题需要弄清楚

  • 为何src_index的偏移需要增加l[target_index - 1]
  • 为何target_index的偏移需要回到l[target_index - 1]

target_index的偏移

先说这个,为何要回到l[target_index - 1]的位置,其实就是因为前面所说道的前后缀的概念。
当前的位置不匹配了,但是前面的内容其实都是匹配的,而匹配的内容,刚好跟前面有重复的前缀,所以,target_index 需要回到之前前缀的后面的位置,也就是l[target_index - 1]的位置。

src_index的偏移

因为l[target_index - 1]的偏移位置已经变化了,所以src_index也必须进行变化,才能够让我们匹配的位置对应起来。

举例来说:
假设匹配串为abcaabcaabcab,模式串为abcab,第一次匹配,在src_index = 0,target_index = 4时出现了第一次不匹配,但是abcab的l[target_index - 1] = 1,所以src_index挪移的距离就应该为3,target_index也回到了l[target_index - 1]的位置
这样就保证了匹配总能够保证最少,可以不进行重复匹配

原文地址:https://www.cnblogs.com/qitian1/p/6461588.html