在字符串s中寻找模式串p的位置,这是一个字符串匹配问题。

举例说明:

 i = 0   1   2   3   4   5   6  7   8   9  10  11  12  13
s = a b a a c a b a a a b a a b
p = a b a a b
j = 0 1 2 3 4

在kmp算法发明之前。人们使用这种算法:

'''
原始的求子串位置算法,O( m * n )
'''
def string_index_of( pstr, pattern, pos = 0 ):
str_index = pos
pattern_index = 0
pattern_len = len( pattern )
while str_index < len( pstr ) and pattern_index < pattern_len:
if pstr[ str_index ] == pattern[ pattern_index ]:
str_index += 1
pattern_index += 1
else:
str_index = str_index - pattern_index + 1
pattern_index = 0
if pattern_index == pattern_len:
return str_index - pattern_index
return -1 pstr = 'i am caochao, i love coding!'
pattern = 'ao'
print( string_index_of( pstr, pattern, 7 ) )
print( pstr.find( pattern ) )

当s[4]与p[4]失配时,主串s回溯到i=1,模式串回溯到j=0,然后从此位置继续匹配。显然这种做法效率低下,假设s长度为n,p长度为m,则其时间复杂度为O(m*n)。

现考虑这样一个问题,当s与p匹配到位置i,j处,s[j]不等于p[j],如果此时保持i不变,p串中从k(0<k<j)处继续匹配,这样无需回溯i的做法这就是我们要讲到的kmp算法。那么应当如何取得这个k值?可以预先求出p中每个失配的j处需要跳转到的位置k(next[j]),这就是kmp算法中的next数组。

kmp算法步骤如下:

1,初始化i,j均为0,

2,依次往后比较s[i]与p[j],若相等则i,j各自加1,否则保持i不变,j=k(next[j])。若某时刻求得j值为-1,i,j也各自加1然后继续匹配

3,重复步骤2

依据上述分析可知,kmp算法中最关键之处在于k值的取法。在匹配进行到s[i]不等于p[j]时,假设应当让s[i]与p[k]继续比较(0<k<j),那么p中前k个字符必须满足,且不能存在k'>k满足等式1:

等式1,p[0,k-1]=s[i-k,i-1]

而在i,j之前已经匹配的字符里存在等式2:

等式2,p[j-k,j-1]=s[i-k,i-1]

由此,可以推出等式3:

等式3,p[0,k-1]=p[j-k,j-1]

至此,k值的取法已经非常明显,即在p串中取最大的k(0<k<j),使得p中开始的k个字符与p[j]之前的k个字符相等。这样就可在s[i]不等于p[j]时,尽可能在距离p[0]远的位置处继续匹配,从而提高匹配效率。

从上面的分析中可以给出k,即next[j]的定义:

1,j=0时,next[j]=-1

2,next[j] = max{k|0<k<j且p[0,k-1]=p[j-k,j-1]}

3,其它情况,next[j]=1

递推求next数组:

从next[j]的定义出发,可以采用递推的方式求得next[j]:

首先,next[0]=-1

令next[j]=k(0<k<j),则表明在p中存在k,且不存在k'>k满足下列关系:

p[0,k-1]=p[j-k,j-1]

那么next[j+1]的取值有3种情况,

1,若p[k]=p[j],则表明在p中存k,且不存在k'>k满足关系p[0,k]=p[j-k,j],那么next[j+1]=k+1,即

next[j+1]=next[j]+1

2,若p[k]不等于p[j],此时可把求next函数的过程看成模式匹配的过程,即p既是主串又是模式串。而在模式匹配过种中,此时应当让p[j]与p[next[k]]继续比较。

为了便于理解,这里令next[k]=k'。

若p[j]=p[k']时,next[j+1]=k'+1,即next[j+1]=next[k]+1,也即

next[j+1]=next[next[j]]+1

同理若p[j]不等于p[k'],那么继续让p[j]与next[k']比较,依次类推,直至k'=-1时:

next[j+1]=0

代码实现如下:

'''
kmp求next[j]数组
'''
def kmp_get_next( pattern ):
i = 0
j = -1
_next = [ 0 ] * len( pattern )
_next[ 0 ] = -1
while i < len( pattern ) - 1:
if j == -1 or pattern[ i ] == pattern[ j ]:
i += 1
j += 1
_next[ i ] = j
else:
j = _next[ j ]
return _next

优化的求next数组方法:

考虑如下模式串:

j       =   0    1    2    3    4
p = a a a a b
next[j] = -1 0 1 2 3

若某时刻s[i]与p[3]不相等,依据next[3]指示应当让s[i]与p[2]继续比较,因p[3]与p[2]相等,这一步明显是多余的。推广到普遍情况,在求next数组过程中,如果next[i]=j且p[i]=p[j],那么令next[i]=next[j]。代码如下:

'''
kmp求next[j]数组
'''
def kmp_get_next( pattern ):
i = 0
j = -1
_next = [ 0 ] * len( pattern )
_next[ 0 ] = -1
while i < len( pattern ) - 1:
if j == -1 or pattern[ i ] == pattern[ j ]:
i += 1
j += 1
if pattern[ i ] == pattern[ j ]:
_next[ i ] = _next[ j ]
else:
_next[ i ] = j
else:
j = _next[ j ]
return _next

预先求得next[j]数组后,kmp算法代码实现如下:

'''
kmp求子串位置
'''
def kmp_index_of( pstr, pattern, pos = 0 ):
_next = kmp_get_next( pattern )
str_index = pos
pattern_index = 0
pattern_len = len( pattern )
while str_index < len( pstr ) and pattern_index < pattern_len:
if pattern_index == -1 or pstr[ str_index ] == pattern[ pattern_index ]:
str_index += 1
pattern_index += 1
else:
pattern_index = _next[ pattern_index ]
if pattern_index == pattern_len:
return str_index - pattern_index
return -1 pstr = 'i am caochao, i love coding!'
pattern = 'ao'
print( kmp_index_of( pstr, pattern, 7 ) )
print( pstr.find( pattern ) )

最后,对比下普通算法与kmp算法解决本文开始提出的问题匹配过程:

普通算法:

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
s = a b a a c a b a a a b a a b
p = a b a a b
j = 0 1 2 3 4

s[4]不等于p[4],令i=1,j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
s = a b a a c a b a a a b a a b
p = a b a a b
j = 0 1 2 3 4

s[1]不等于p[0],令i=2,j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
s = a b a a c a b a a a b a a b
p = a b a a b
j = 0 1 2 3 4

s[3]不等于p[1],令i=3,j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
s = a b a a c a b a a a b a a b
p = a b a a b
j = 0 1 2 3 4

限于篇幅,略过中间n步,跳至i=9,j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
s = a b a a c a b a a a b a a b
p = a b a a b
j = 0 1 2 3 4

一路i++,j++,直到i=14,跳出循环结束匹配,并返回9。

kmp算法:

p串next数组为:

j       =   0    1    2    3    4
p = a b a a b
next[j] = -1 0 0 1 1

next数组优化过后变为:

j       =   0    1    2    3    4
p = a b a a b
next[j] = -1 0 -1 1 0

下面开始匹配:

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
s = a b a a c a b a a a b a a b
p = a b a a b
j = 0 1 2 3 4

s[4]不等于p[4],令j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
s = a b a a c a b a a a b a a b
p = a b a a b
j = 0 1 2 3 4

s[4]不等于p[0],next[0]=-1,因此i,j各自加1。i=5,j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
s = a b a a c a b a a a b a a b
p = a b a a b
j = 0 1 2 3 4

i++,j++,直到s[9]不等于p[4],令j=0

 i = 0   1   2   3   4   5   6   7   8   9  10  11  12  13
s = a b a a c a b a a a b a a b
p = a b a a b
j = 0 1 2 3 4

一路i++,j++,直到i=14,跳出循环结束匹配,并返回9。

通过对比可以看出,kmp算法比普通算法快得多,只要预先求出模式串next数组,则整个匹配过程中i无需回溯,时间复杂度亦由普通算法O(m*n)提升为O(m+n)。

kmp算法简明教程的更多相关文章

  1. 【★】KMP算法完整教程

    KMP算法完整教程 全称:                               Knuth_Morris_Pratt Algorithm(KMP算法) 类型:                 ...

  2. 【★】KMP算法完整教程

    KMP算法完整教程 全称:                               Knuth_Morris_Pratt Algorithm(KMP算法) 类型:                 ...

  3. KMP算法完整教程 (上)

    KMP算法完整教程 全称: Knuth_Morris_Pratt Algorithm(KMP算法) 类型: 高级检索算法 功能: 字符串匹配查找 提出者: D.E.Knuth(克努兹),J.H.Mor ...

  4. !KMP算法完整教程

      KMP算法完整教程 全称:                               Knuth_Morris_Pratt Algorithm(KMP算法) 类型:                ...

  5. KMP算法完整教程 (下)

    下面我们用数学归纳法来解决这个填值的问题. 这里我们借鉴数学归纳法的三个步骤(或者说是动态规划?): 1.初始状态 2.假设第j位以及第j位之前的我们都填完了 3.推论第j+1位该怎么填 初始状态我们 ...

  6. KMP算法简明法则

    KMP算法也算是相当经典,但是对于初学者来说确实有点绕,大学时候弄明白过后来几年不看又忘记了,然后再弄明白过了两年又忘记了,好在之前理解到了关键点,看了一遍马上又能理解上来.关于这个算法的详解网上文章 ...

  7. Lisp简明教程

    此教程是我花了一点时间和功夫整理出来的,希望能够帮到喜欢Lisp(Common Lisp)的朋友们.本人排版很烂还望多多海涵! <Lisp简明教程>PDF格式下载 <Lisp简明教程 ...

  8. KMP算法(研究总结,字符串)

    KMP算法(研究总结,字符串) 前段时间学习KMP算法,感觉有些复杂,不过好歹是弄懂啦,简单地记录一下,方便以后自己回忆. 引入 首先我们来看一个例子,现在有两个字符串A和B,问你在A中是否有B,有几 ...

  9. Vbs 脚本编程简明教程之一

    —为什么要使用 Vbs ? 在 Windows 中,学习计算机操作也许很简单,但是很多计算机工作是重复性劳动,例如你每周也许需要对一些计算机文件进行复制.粘贴.改名.删除,也许你每天启动 计算机第一件 ...

随机推荐

  1. Yarn中的几种状态机

    1 概述 为了增大并发性,Yarn采用事件驱动的并发模型,将各种处理逻辑抽象成事件和调度器,将事件的处理过程用状态机表示.什么是状态机? 如果一个对象,其构成为若干个状态,以及触发这些状态发生相互转移 ...

  2. [札记]IL经典指令解析之方法调度

    call.callvirt和calli指令用于完成方法调用,有何区别呢? 1)call使用静态调度,也就是根据引用类型的静态类型来调度方法.call指令根据引用变量的类型来调用方法,因此通常用于调用非 ...

  3. 关于使用base36的技巧 生成 优惠券兑换码的相关讨论

    关于优惠券的生成后台的制作问题,已经拖了很久了还没有合并.但是持续暴露出来的问题 也很多,我的代码以及前面的一个人的代码被持续review暴露出了大量问题.昨天晚上在

  4. 用python实现远程复制 (scp + expect )

    scp 功能很强大,但需要人工输入 password, 当然可以通过把 公钥保存在远程主机的 ~/.ssh 目录中,而后就不用输入password,但这需要配置. 用 sshpass 可能在命令输入 ...

  5. JS制作的简单的三级及联

    前台: <form id="form1" runat="server"> <div> 省 <select id="Pro ...

  6. c++/java/c# 几种编程语言的指针、引用比较

    前一段时间,我在 cnblogs 别人的博客中,谈到: java 中的引用/指针,与 c++/C# 中的引用/指针不是一个概念. Java 引用,相当于 c++ 指针(fun3).Java 引用可以赋 ...

  7. Oracle:递归查询(树形结构数据)

    今天要做一个查询功能:查询某用户所属部门,且包含该部门的所有上级部门信息.偶然找到了一个方法,特意来做个笔记.分享给和我一样的菜鸟,哈哈 查询子节点 1 select * 2 from d_arc_d ...

  8. 重学HTML

    http://www.imooc.com/learn/9 1.em/strong 如果想在一段话中特别强调某几个文字,这时候就可以用到<em>或<strong>标签. 但两者在 ...

  9. DIV+CSS规范命名大全集合

    (从已经死了一次又一次终于挂掉的百度空间人工抢救出来的,发表日期 2014-06-19)   网页制作中规范使用DIV+CSS命名规则,可以改善优化功效特别是团队合作时候可以提供合作制作效率,具体DI ...

  10. 使用U盘安装Ubuntu系统的实践小结

    参考教程:http://diybbs.zol.com.cn/1/33925_1942.html   遇到的问题:安装ubuntu 12.04 64位,提示缺少“/casper/vmlinuz.efi ...