字符串匹配的kmp算法 及 python实现

一:背景

给定一个主串(以 S 代替)和模式串(以 P 代替),要求找出 P 在 S 中出现的位置,此即串的模式匹配问题。html

Knuth-Morris-Pratt 算法(简称 KMP)是解决这一问题的经常使用算法之一,这个算法是由高德纳(Donald Ervin Knuth)和沃恩 · 普拉特在 1974 年构思,同年詹姆斯 ·H· 莫里斯也独立地设计出该算法,最终三人于 1977 年联合发表。python

在继续下面的内容以前,有必要在这里介绍下两个概念:真前缀 和 真后缀算法

由上图所得, "真前缀" 指除了自身之外,一个字符串的所有头部组合;"真后缀" 指除了自身之外,一个字符串的所有尾部组合。数组

二:朴素字符串匹配算法

初遇串的模式匹配问题,咱们脑海中的第一反应,就是朴素字符串匹配(即所谓的暴力匹配)app

暴力匹配的时间复杂度为 O(nm),其中 n 为 S 的长度,m 为 P 的长度。很明显,这样的时间复杂度很难知足咱们的需求。post

接下来进入正题:时间复杂度为 Θ(n+m) 的 KMP 算法。spa

三:KMP 字符串匹配算法

3.1 算法流程

如下摘自阮一峰的字符串匹配的 KMP 算法,并做稍微修改。.net

(1)设计

首先,主串 "BBC ABCDAB ABCDABCDABDE" 的第一个字符与模式串 "ABCDABD" 的第一个字符,进行比较。由于 B 与 A 不匹配,因此模式串后移一位。3d

(2)

由于 B 与 A 又不匹配,模式串再日后移。

(3)

就这样,直到主串有一个字符,与模式串的第一个字符相同为止。

(4)

接着比较主串和模式串的下一个字符,仍是相同。

(5)

直到主串有一个字符,与模式串对应的字符不相同为止。

(6)

这时,最天然的反应是,将模式串整个后移一位,再从头逐个比较。这样作虽然可行,可是效率不好,由于你要把 "搜索位置" 移到已经比较过的位置,重比一遍。

(7)

一个基本事实是,当空格与 D 不匹配时,你实际上是已经知道前面六个字符是 "ABCDAB"。KMP 算法的想法是,设法利用这个已知信息,不要把 "搜索位置" 移回已经比较过的位置,而是继续把它向后移,这样就提升了效率。

(8)

i 0 1 2 3 4 5 6 7
模式串 A B C D A B D '\0'
next[i] -1 0 0 0 0 1 2 0

怎么作到这一点呢?能够针对模式串,设置一个跳转数组int next[],这个数组是怎么计算出来的,后面再介绍,这里只要会用就能够了。

(9)

已知空格与 D 不匹配时,前面六个字符 "ABCDAB" 是匹配的。根据跳转数组可知,不匹配处 D 的 next 值为 2,所以接下来从模式串下标为 2 的位置开始匹配

(10)

由于空格与C不匹配,C 处的 next 值为 0,所以接下来模式串从下标为 0 处开始匹配。

(11)

由于空格与 A 不匹配,此处 next 值为 - 1,表示模式串的第一个字符就不匹配,那么直接日后移一位。

(12)

逐位比较,直到发现 C 与 D 不匹配。因而,下一步从下标为 2 的地方开始匹配。

(13)

逐位比较,直到模式串的最后一位,发现彻底匹配,因而搜索完成。

3.2 next 数组是如何求出的展开目录

next 数组的求解基于 “真前缀” 和 “真后缀”,即next[i]等于P[0]...P[i - 1]最长的相同真先后缀的长度(请暂时忽视 i 等于 0 时的状况,下面会有解释)。咱们依旧以上述的表格为例,为了方便阅读,我复制在下方了。

i 0 1 2 3 4 5 6 7
模式串 A B C D A B D '\0'
next[i] -1 0 0 0 0 1 2 0
  1. i = 0,对于模式串的首字符,咱们统一为next[0] = -1
  2. i = 1,前面的字符串为A,其最长相同真先后缀长度为 0,即next[1] = 0
  3. i = 2,前面的字符串为AB,其最长相同真先后缀长度为 0,即next[2] = 0
  4. i = 3,前面的字符串为ABC,其最长相同真先后缀长度为 0,即next[3] = 0
  5. i = 4,前面的字符串为ABCD,其最长相同真先后缀长度为 0,即next[4] = 0
  6. i = 5,前面的字符串为ABCDA,其最长相同真先后缀为A,即next[5] = 1
  7. i = 6,前面的字符串为ABCDAB,其最长相同真先后缀为AB,即next[6] = 2
  8. i = 7,前面的字符串为ABCDABD,其最长相同真先后缀长度为 0,即next[7] = 0

那么,为何根据最长相同真先后缀的长度就能够实如今不匹配状况下的跳转呢?举个表明性的例子:假如i = 6时不匹配,此时咱们是知道其位置前的字符串为ABCDAB,仔细观察这个字符串,首尾都有一个AB,既然在i = 6处的 D 不匹配,咱们为什么不直接把i = 2处的 C 拿过来继续比较呢,由于都有一个AB啊,而这个AB就是ABCDAB的最长相同真先后缀,其长度 2 正好是跳转的下标位置。

python实现,以下:

def partial_table(p):
    '''''partial_table("ABCDABD") -> [0, 0, 0, 0, 1, 2, 0]'''
    prefix = set()
    postfix = set()
    ret = [0]
    for i in range(1, len(p)):
        prefix.add(p[:i])
        postfix = {p[j:i + 1] for j in range(1, i + 1)}
        ret.append(len((prefix & postfix or {''}).pop()))
    return ret
print partial_table("ABCDABD")
#[0, 0, 0, 0, 1, 2, 0]

所有代码:

#coding=utf-8
def kmp_match(s, p):
    m = len(s);
    n = len(p)
    cur = 0  # 起始指针cur
    table = partial_table(p)
    while cur <= m - n:     #只去匹配前m-n个
        for i in range(n):
            if s[i + cur] != p[i]:
                cur += max(i - table[i - 1], 1)  # 有了部分匹配表,咱们不仅是单纯的1位1位往右移,能够一次移动多位
                break
        else:           #for 循环中,若是没有从任何一个 break 中退出,则会执行和 for 对应的 else
                        #只要从 break 中退出了,则 else 部分不执行。
            return True
    return False


# 部分匹配表
def partial_table(p):
    '''''partial_table("ABCDABD") -> [0, 0, 0, 0, 1, 2, 0]'''
    prefix = set()
    postfix = set()
    ret = [0]
    for i in range(1, len(p)):
        prefix.add(p[:i])
        postfix = {p[j:i + 1] for j in range(1, i + 1)}
        ret.append(len((prefix & postfix or {''}).pop()))
    return ret


print partial_table1("ABCDABD")

print kmp_match("BBC ABCDAB ABCDABCDABDE", "ABCDABD")

 

参考   如何理解 KMP     

KMP 算法精解及其 Python 版的代码示例

相关文章
相关标签/搜索