浅谈Python描述数据结构之KMP篇


Posted in Python onSeptember 06, 2020

前言

  本篇章主要介绍串的KMP模式匹配算法及其改进,并用Python实现KMP算法。

1. BF算法

  BF算法,即BruceForceBruce-ForceBruce−Force算法,又称暴力匹配算法。其思想就是将主串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。

  假设主串S=ABACABABS=ABACABABS=ABACABAB,模式串T=ABABT=ABABT=ABAB,每趟匹配失败后,主串S指针回溯,模式串指针回到头部,然后再次匹配,过程如下:

浅谈Python描述数据结构之KMP篇

def BF(substrS, substrT):
  if len(substrT) > len(substrS):
    return -1
  j = 0
  t = 0
  while j < len(substrS) and t < len(substrT):
    if substrT[t] == substrS[j]:
      j += 1
      t += 1
    else:
      j = j - t + 1
      t = 0
  if t == len(substrT):
    return j - t
  else:
    return -1

2. KMP算法

  KMP算法,是由D.E.KnuthJ.H.MorrisV.R.PrattD.E.Knuth、J.H.Morris、V.R.PrattD.E.Knuth、J.H.Morris、V.R.Pratt同时发现的,又被称为克努特-莫里斯-普拉特算法。该算法的基本思路就是在匹配失败后,无需回到主串和模式串最近一次开始比较的位置,而是在不改变主串已经匹配到的位置的前提下,根据已经匹配的部分字符,从模式串的某一位置开始继续进行串的模式匹配。

  就是这次匹配失败时,下次匹配时模式串应该从哪一位开始比较。

  BF算法思路简单,便于理解,但是在执行时效率太低。在上述的匹配过程中,第一次匹配时已经匹配的"ABA""ABA""ABA",其前缀与后缀都是"A""A""A",这个时候我们就不需要执行第二次匹配了,因为第一次就已经匹配过了,所以可以跳过第二次匹配,直接进行第三次匹配,即前缀位置移到后缀位置,主串指针无需回溯,并继续从该位开始比较。

  前缀:是指除最后一个字符外,字符串的所有头部子串。
  后缀:是指除第一个字符外,字符串的所有尾部子串。
  部分匹配值(Partial(Partial(Partial Match,PM)Match,PM)Match,PM):字符串的前缀和后缀的最长相等前后缀长度。
  例如,a'a'′a′的前缀和后缀都为空集,则最长公共前后缀长度为0;ab'ab'′ab′的前缀为{a}\{a\}{a},后缀为{b}\{b\}{b},则最长公共前后缀为空集,其长度长度为0;aba'aba'′aba′的前缀为{a,ab}\{a,ab\}{a,ab},后缀为{a,ba}\{a,ba\}{a,ba},则最长公共前后缀为{a}\{a\}{a},其长度长度为1;abab'abab'′abab′的前缀为{a,ab,aba}\{a,ab,aba\}{a,ab,aba},后缀为{b,ab,bab}\{b,ab,bab\}{b,ab,bab},则最长公共前后缀为{ab}\{ab\}{ab},其长度长度为2。
  前缀一定包含第一个字符,后缀一定包含最后一个字符。

浅谈Python描述数据结构之KMP篇

 如果模式串1号位与主串当前位(箭头所指的位置)不匹配,将模式串1号位与主串的下一位进行比较。next[0]=-1,这边就是一个特殊位置了,即如果主串与模式串的第1位不相同,那么下次就直接比较各第2位的字符。

浅谈Python描述数据结构之KMP篇

 如果模式串2号位与主串当前位不匹配,找最长公共前后缀,指针前面的子串为"A""A""A",即最长公共前后缀为空集,其长度为0,则下次匹配时将模式串1号位与主串的当前位进行比较。next[1]=0

浅谈Python描述数据结构之KMP篇

  如果模式串3号位与主串当前位不匹配,找最长公共前后缀,指针前面的子串为"AB""AB""AB",即最长公共前后缀为空集,其长度为0,则下次匹配时将模式串1号位与主串的当前位进行比较。next[2]=0

浅谈Python描述数据结构之KMP篇

 如果模式串4号位与主串当前位不匹配,找最长公共前后缀,指针前面的子串为"ABA""ABA""ABA",即最长公共前后缀为"A""A""A",其长度为1,则下次匹配时将前缀位置移到后缀位置,即模式串2号位与主串的当前位进行比较。next[3]=1

浅谈Python描述数据结构之KMP篇

  如果模式串5号位与主串当前位不匹配,找最长公共前后缀,指针前面的子串为"ABAA""ABAA""ABAA",即最长公共前后缀为"A""A""A",其长度为1,则下次匹配时将前缀位置移到后缀位置,即模式串2号位与主串的当前位进行比较。next[4]=1

浅谈Python描述数据结构之KMP篇

  如果模式串6号位与主串当前位不匹配,找最长公共前后缀,指针前面的子串为"ABAAB""ABAAB""ABAAB",即最长公共前后缀为"AB""AB""AB",其长度为2,则下次匹配时将前缀位置移到后缀位置,即模式串3号位与主串的当前位进行比较。next[5]=2

浅谈Python描述数据结构之KMP篇

  如果模式串7号位与主串当前位不匹配,找最长公共前后缀,指针前面的子串为"ABAABC""ABAABC""ABAABC",即最长公共前后缀为空集,其长度为0,则下次匹配时将模式串1号位与主串的当前位进行比较。next[6]=0

浅谈Python描述数据结构之KMP篇  

如果模式串8号位与主串当前位不匹配,找最长公共前后缀,指针前面的子串为"ABAABCA""ABAABCA""ABAABCA",即最长公共前后缀为"A""A""A",其长度为1,则下次匹配时将模式串2号位与主串的当前位进行比较。next[7]=1

  综上,可以得到模式串的next数组,发现没有,把主串去掉也可以得到这个数组,即下次匹配时模式串向后移动的位数与主串无关,仅与模式串本身有关。

位编号 1 2 3 4 5 6 7 8
索引 0 1 2 3 4 5 6 7
模式串 A B A A B C A C
next -1 0 0 1 1 2 0 1

  next数组,即存放的是每个字符匹配失败时,对应的下一次匹配时模式串开始匹配的位置。

  如何在代码里实现上述流程呢?举个栗子,蓝色方框圈出的就是公共前后缀,假设next[j]=t

浅谈Python描述数据结构之KMP篇

 当Tj=TtT_j=T_tTj​=Tt​时,可以得到next[j+1]=t+1=next[j]+1next[j+1]=t+1=next[j]+1next[j+1]=t+1=next[j]+1。这个时候j=4,t=1j=4,t=1j=4,t=1(索引);

浅谈Python描述数据结构之KMP篇

  当TjTtT_j \neq T_tTj​?​=Tt​时,即模式串ttt位置与主串(并不是真正的主串)不匹配,则将下面的那个模式串移动到next[t]next[t]next[t]位置进行比较,即t=next[t]t=next[t]t=next[t],直到Tj=TtT_j=T_tTj​=Tt​或t=1t=-1t=−1,当t=1t=-1t=−1时,next[j+1]=0next[j+1]=0next[j+1]=0。这里就是t=next[2]=0t=next[2]=0t=next[2]=0,即下次匹配时,模式串的第1位与主串当前位进行比较。

  代码如下:

def getNext(substrT):
  next_list = [-1 for i in range(len(substrT))]
  j = 0
  t = -1
  while j < len(substrT) - 1:
    if t == -1 or substrT[j] == substrT[t]:
      j += 1
      t += 1
      # Tj=Tt, 则可以到的next[j+1]=t+1
      next_list[j] = t
    else:
      # Tj!=Tt, 模式串T索引为t的字符与当前位进行匹配
      t = next_list[t]
  return next_list


def KMP(substrS, substrT, next_list):
  count = 0
  j = 0
  t = 0
  while j < len(substrS) and t < len(substrT):
    if substrS[j] == substrT[t] or t == -1:
      # t == -1目的就是第一位匹配失败时
      # 主串位置加1, 匹配串回到第一个位置(索引为0)
      # 匹配成功, 主串和模式串指针都后移一位
      j += 1
      t += 1
    else:
      # 匹配失败, 模式串索引为t的字符与当前位进行比较
      count += 1
      t = next_list[t]
  if t == len(substrT):
    # 这里返回的是索引
    return j - t, count+1
  else:
    return -1, count+1

3. KMP算法优化版

  上面定义的next数组在某些情况下还有些缺陷,发现没有,在第一个图中,我们还可以跳过第3次匹配,直接进行第4次匹配。为了更好地说明问题,我们以下面这种情况为例,来优化一下KMP算法。假设主串S=AAABAAAABS=AAABAAAABS=AAABAAAAB,模式串T=AAAABT=AAAABT=AAAAB,按照KMP算法,匹配过程如下:

浅谈Python描述数据结构之KMP篇

 可以看到第2、3、4次的匹配是多余的,因为我们在第一次匹配时,主串SSS的4号位为模式串TTT的4号位就已经比较了,且T3S3T_3 \neq S_3T3​?​=S3​,又因为模式串TTT的4号位与其1、2、3号位的字符一样,即T3=T2=T1=T0S3T_3=T_2=T_1=T_0 \neq S_3T3​=T2​=T1​=T0​?​=S3​,所以可以直接进入第5次匹配。

  那么,问题出在哪里???我们结合着next数组看一下:

位编号 1 2 3 4 5
索引 0 1 2 3 4
模式串 A A A A B
next -1 0 1 2 3

  问题在于,当TjSjT_j \neq S_jTj​?​=Sj​时,下次匹配的必然是Tnext[j]T_{next[j]}Tnext[j]​与SjS_jSj​,如果这时Tnext[j]=TjT_{next[j]} = T_jTnext[j]​=Tj​,那么又相当于TjT_jTj​与SjS_jSj​进行比较,因为它们的字符一样,毫无疑问,这次匹配是没有意义的,应当将next[j]next[j]next[j]的值直接赋值为-1,即遇到这种情况,主串与模式串都从下一位开始比较。

  所以,我们要修正一下next数组。

  大致流程和上面求解next数组时一样,这里就是多了一个判别条件,如果在匹配时出现了Tnext[j]=TjT_{next[j]} = T_jTnext[j]​=Tj​,我们就将next[j]更新为next[\Big[[next[j]]\Big]],直至两者不相等为止(相当于了迭代)。在代码里面实现就是,如果某个字符已经相等或者第一个next[j]数组值为-1(即t=1t=-1t=−1),且主串和模式串指针各后移一位时的字符仍然相同,那么就将当前的next[j]值更新为上一个next[j]数组值,更新后的数组命名为nextval

  代码如下:

def getNextval(substrT):
  nextval_list = [-1 for i in range(len(substrT))]
  j = 0
  t = -1
  while j < len(substrT) - 1:
    if t == -1 or substrT[j] == substrT[t]:
      j += 1
      t += 1
      if substrT[j] != substrT[t]:
        # Tj=Tt, 但T(j+1)!=T(t+1), 这个就和next数组计算时是一样的
        # 可以得到nextval[j+1]=t+1
        nextval_list[j] = t
      else:
        # Tj=Tt, 且T(j+1)==T(t+1), 这个就是next数组需要更新的
        # nextval[j+1]=上一次的nextval_list[t]
        nextval_list[j] = nextval_list[t]
    else:
      # 匹配失败, 模式串索引为t的字符与当前位进行比较
      t = nextval_list[t]
  return nextval_list

  对KMP的优化其实就是对next数组的优化,修正后的next数组,即nextval数组如下:

位编号 1 2 3 4 5
索引 0 1 2 3 4
模式串 A A A A B
nextval -1 -1 -1 -1 3

  下面就测试一下:

if __name__ == '__main__':
  S1 = 'ABACABAB'
  T1 = 'ABAB'
  S2 = 'AAABAAAAB'
  T2 = 'AAAAB'

  print('*' * 50)
  print('主串S={0}与模式串T={1}进行匹配'.format(S1, T1))

  print('{:*^25}'.format('KMP'))
  next_list1 = getNext(T1)
  print('next数组为: {}'.format(next_list1))
  index1_1, count1_1 = KMP(S1, T1, next_list1)
  print('匹配到的位置(索引): {}, 匹配次数: {}'.format(index1_1, count1_1))

  print('{:*^25}'.format('KMP优化版'))
  nextval_list1 = getNextval(T1)
  print('nextval数组为: {}'.format(nextval_list1))
  index1_2, count1_2 = KMP(S1, T1, nextval_list1)
  print('匹配到的位置(索引): {}, 匹配次数: {}'.format(index1_2, count1_2))

  print('')
  print('*' * 50)
  print('主串S={0}与模式串T={1}进行匹配'.format(S2, T2))

  print('{:*^25}'.format('KMP'))
  next_list2 = getNext(T2)
  print('next数组为: {}'.format(next_list2))
  index2_1, count2_1 = KMP(S2, T2, next_list2)
  print('匹配到的位置(索引): {}, 匹配次数: {}'.format(index2_1, count2_1))

  print('{:*^25}'.format('KMP优化版'))
  nextval_list2 = getNextval(T2)
  print('nextval数组为: {}'.format(nextval_list2))
  index2_2, count2_2 = KMP(S2, T2, nextval_list2)
  print('匹配到的位置(索引): {}, 匹配次数: {}'.format(index2_2, count2_2))

  运行结果如下:

浅谈Python描述数据结构之KMP篇

  运行的结果和我们分析的是一样的,不修正next数组时,主串S=ABACABABS=ABACABABS=ABACABAB与模式串T=ABABT=ABABT=ABAB匹配时需要4次,主串S=AAABAAAABS=AAABAAAABS=AAABAAAAB与模式串T=AAAABT=AAAABT=AAAAB匹配时需要5次;修正next数组后,主串S=ABACABABS=ABACABABS=ABACABAB与模式串T=ABABT=ABABT=ABAB匹配时需要3次,主串S=AAABAAAABS=AAABAAAABS=AAABAAAAB与模式串T=AAAABT=AAAABT=AAAAB匹配时仅需要2次。

结束语

  在写本篇博客之前也是反复看参考书、视频,边画图边去理解它,这篇博客也是反复修改了好几次,最终算是把KMP解决掉了,有关字符串知识的复习也算是基本结束,下面就是刷题了(虽然在LeetCode做过了几道题)。

到此这篇关于Python描述数据结构之KMP篇的文章就介绍到这了,更多相关Python KMP内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
python smtplib模块发送SSL/TLS安全邮件实例
Apr 08 Python
探究Python中isalnum()方法的使用
May 18 Python
浅谈python函数之作用域(python3.5)
Oct 27 Python
python使用循环打印所有三位数水仙花数的实例
Nov 13 Python
Python 串口读写的实现方法
Jun 12 Python
python文本数据处理学习笔记详解
Jun 17 Python
python爬虫实现中英翻译词典
Jun 25 Python
Python序列对象与String类型内置方法详解
Oct 22 Python
python实现滑雪游戏
Feb 22 Python
python GUI库图形界面开发之PyQt5选项卡控件QTabWidget详细使用方法与实例
Mar 01 Python
Python PyQt5运行程序把输出信息展示到GUI图形界面上
Apr 27 Python
了解一下python内建模块collections
Sep 07 Python
详解Python3 定义一个跨越多行的字符串的多种方法
Sep 06 #Python
Python中实现一行拆多行和多行并一行的示例代码
Sep 06 #Python
Pytest单元测试框架如何实现参数化
Sep 05 #Python
Python实例方法、类方法、静态方法区别详解
Sep 05 #Python
Python装饰器如何实现修复过程解析
Sep 05 #Python
Python JSON常用编解码方法代码实例
Sep 05 #Python
Python直接赋值及深浅拷贝原理详解
Sep 05 #Python
You might like
function.inc.php超越php
2006/12/09 PHP
快速配置PHPMyAdmin方法
2008/06/05 PHP
thinkPHP框架实现的无限回复评论功能示例
2018/06/09 PHP
利用PHP内置SERVER开启web服务(本地开发使用)
2020/01/22 PHP
经常用的图片在容器中的水平垂直居中实例
2007/06/10 Javascript
JavaScript 字符串乘法
2009/08/20 Javascript
JavaScript中的setUTCDate()方法使用详解
2015/06/11 Javascript
利用jquery获取select下拉框的值
2016/11/23 Javascript
JavaScript实现重力下落与弹性效果的方法分析
2017/12/20 Javascript
webpack4 处理CSS的方法示例
2018/09/03 Javascript
使用Object.defineProperty如何巧妙找到修改某个变量的准确代码位置
2018/11/02 Javascript
Vue服务端渲染实践之Web应用首屏耗时最优化方案
2019/03/22 Javascript
对layui数据表格动态cols(字段)动态变化详解
2019/10/25 Javascript
js实现轮播图效果 z-index实现轮播图
2020/01/17 Javascript
python操作excel的包(openpyxl、xlsxwriter)
2018/06/11 Python
浅谈python在提示符下使用open打开文件失败的原因及解决方法
2018/11/30 Python
Django RBAC权限管理设计过程详解
2019/08/06 Python
python  ceiling divide 除法向上取整(或小数向上取整)的实例
2019/12/27 Python
Django 批量插入数据的实现方法
2020/01/12 Python
Python2.7:使用Pyhook模块监听鼠标键盘事件-获取坐标实例
2020/03/14 Python
pytorch快速搭建神经网络_Sequential操作
2020/06/17 Python
解决python中0x80072ee2错误的方法
2020/07/19 Python
世界顶级俱乐部的官方球衣和套装:Subside Sports
2018/04/22 全球购物
英国在线购买轮胎、预订汽车、汽车维修和装配网站:Protyre
2020/04/12 全球购物
编写一子程序,将一链表倒序,即使链表表尾变表头,表头变表尾
2016/02/10 面试题
应届生财务会计求职信
2013/11/05 职场文书
委托书模板
2014/04/04 职场文书
学生请假条
2014/04/11 职场文书
医院竞聘演讲稿
2014/05/16 职场文书
2014最新开业庆典策划方案(5篇)
2014/09/15 职场文书
党员教师四风自我剖析材料
2014/09/30 职场文书
个人租房协议书样本
2014/10/01 职场文书
医生见习报告范文
2014/11/03 职场文书
未婚证明格式
2015/06/15 职场文书
python scipy 稀疏矩阵的使用说明
2021/05/26 Python
win10系统xps文件怎么打开?win10打开xps文件的两种操作方法
2022/07/23 数码科技