白话KMP算法,经典字符匹配算法

记录对KMP算法的一些理解

释意:

  • mstr:目标串;主串
  • pstr: 模式串

 

首先,要介绍看毛片KMP算法,就得先介绍什么是字符串匹配算法;

给出问题:给定主串mstr(eg:abababbaaaba)和模式串pstr(eg:bba),如何判断主串中是否含有模式串?

类似这种,判断字符串pstr是否存在于另一字符串mstr中的问题,就是字符串匹配问题;

碰到这类问题,如果是简单/短一些的字符串,可以直接考虑使用暴力匹配法:

转换为代码,

# 暴力匹配法
def bfMatching(mstr, pstr):             # mstr:目标串,pstr:模式串
    mLen, pLen = len(mstr), len(pstr)   # 记录两串长度
    i, j = 0, 0                         # 初始化两串匹配索引值
    while i < mLen and j < pLen:        # 终止循环条件
        if mstr[i] == pstr[j]:          # 字符匹配成功
            i, j = i + 1, j + 1         # 两串索引均向前行进
        else:                           # 字符串失配
            i, j = i - j + 1, 0         # 模式串索引跳转至首位,目标串索引跳转至本轮匹配首字符的下一位
    if j == n:                          # 模式串行进结束,即匹配成功
        return i - j                    # 返回主串中匹配字串首字符位置
    return -1                           # 匹配失败

从理解上来说,可以看作是在目标串mstr上逐位向后推进,即以mstr上每一位字符为基准,与模式串pstr进行匹配;

我想,这从逻辑上理解并不难;

由于暴力匹配法的时间复杂度为O(n2),如果目标串很长,模式串也很长,暴力匹配法的时间资源消耗是巨大的;

 

KMP算法

其实,在字符串匹配的过程中,有很多细节值得考究,上文的图中,由于模式串过短,不容易看出,我们再来张图,你应该能看出些端倪:

子串匹配到最后一个字符,与目标串失配,那么,按照暴力匹配的思路,是将模式串向右移一位,从新开始比较字符串,如下示:

模式串右移一位后,继续失配、失配、失配...,直到模式串右移的第四次,与目标串完美匹配:

让我们回到此次匹配的第一次循环,即模式串前6个字符匹配,在第六个字符“m”处失配;

试想,在匹配过程中,模式串是不是已经获取到目标串的很多信息,例如,模式串失配字符前的字符‘ab’与模式串前两个字符‘ab’相等;

那可不可以在第一次失配后,直接将模式串前两个字符‘ab’移动到目标串失配字符前两个字符‘ab’处?(此处,目标串失配字符前是字符‘ab’,这是可以确定的),当然可以,这就是KMP算法的思想,同时,这种思想涉及到求取字符串的最大相等前缀后缀以及next数组;

 

子串最长相同前缀后缀

什么是最长相同前缀后缀?

我也不打算用文字或者公式来叙述,用图来理解更直观些

 

好了,现在你已经理解什么是子串最长相同前缀后缀了,这对理解KMP算法中的next数组很有帮助;

 

next数组及其求取

next数组的概念与前文所述子串最大相同前缀后缀息息相关;

不同的是,next数组是存放字符串中每一字符(不包括)前最大相同前缀后缀长度的数组,记住,数组存放的是长度(int类型),而不是字符(string类型)。

以表说明:

这下你应该了解next数组了把,注意,next数组首位置为-1,目的是方便代码编写;

了解了这些基本概念,那我们来试试让程序帮我们求对应字符串的next数组把,我先把代码贴上来:

def genPnext(pstr):         # 传入模式串
    pLen = len(pstr)        # 获取长度值
    pnext = [-1] * pLen     # 初始化next表
    i, j = -1, 0            # j沿单方向朝模式串尾部行进;i索引记录已匹配信息,用于回溯
    while j < pLen - 1:     # 设置循环条件,j行进至串尾部结束循环
        if i == -1 or pstr[i] == pstr[j]:       # 右侧条件真,则在上一轮匹配基础上,在pnext表下一空位置值增加1
                                                # 左侧条件为i回溯终止条件,即回溯至记录信息量归零
            i, j = i + 1, j + 1                 # 信息匹配,j向前行进,i增加已匹配信息
            pnext[j] = i                        # pnext新空位值较前位值增加1(基于i,j值匹配条件上)
        else:
            i = pnext[i]                        # i,j处字符失配,j停止向前,i向已记录最长前缀后缀处跳转
    # print(pnext)
    return pnext

乍的一看,有点懵,看看注释,emmm,好像更懵;

没关系,我们继续用图来说明,具象点观察代码每一步的作用以及原理;

规定字符串名称为:str

为保证i与j分开向串的尾部行进,初始化i = -1,j = 0(程序员数数都是从0开始,这没疑问吧~);

职能上看,i 索引负责管理匹配中前缀新字符,j 索引负责管理匹配中后缀新字符;

j 索引用于确定每个字符的next数组值,即next[j],而 i 索引用于每次字符失配后进行回溯,查询记录中最大相同前缀后缀长度并跳转过去,再次与str[j]进行比对(这里是每次用 str[j] 与 str[i]比较,处理后得出的结果填在next[j+1]处,不明白就瞅一眼代码吧);

next的求取是向前递进的,请留意,已得的next数组包含很大的信息量,后方字符next值的求取会非常倚重前方已得的字符next值;

 

接下来,我们进入正题:

1、向串尾部行进的过程中,如果str[i]==str[j],那么认为较上一字符str[j-1]的next值next[j-1],字符str[j]的next值next[j]增加了1,这是基于已得next数组信息的基础上得出的(每一轮对next值的求取,主要观察的是j索引指向的新字符,如果匹配,那么基于前一轮得出的最大相同前缀后缀长度,这一轮加1就好,下给图示)

2、如果str[i]!=str[j],有意思的来了,i值需要立即回溯到位置0吗?也不能断定说不需要,万一next[i]=0呢。这里传达的意思是,当新字符失配时,i 需要利用已取得的next数组,回溯到合适的位置;

当前字符失配,那么 j 立在原地不用动,i 根据 i = next[i]跳转到相应位置,原理是 i 位置虽然与 j 位置失配,但是仍然可能有相同的前缀、后缀,这里视既得next数组而定,我们可能还需要一张图来说明:

现在再或过头看看代码,是不是有不一样的感觉了?

 

KMP算法实现

老规矩,我先贴个代码哈

# KMP匹配算法
def kmpMaching(mstr, pstr):             # mstr:主串;pstr:模式串
    pnext = genPnext(pstr)              # 根据模式串求取pnext数组
    pLen, mLen = len(pstr), len(mstr)   # 记录主串/模式串长度
    i, j = 0, 0                         # 主串/模式串索引指针初始化
    while j < mLen and i < pLen:        # 设置循环边界条件
        if i == -1 or mstr[j] == pstr[i]:       # 右侧条件真,则相应位置字符匹配成功
                                                # 左侧条件为失配后模式串回溯的终止条件,即失配后未在模式串中找到合适子串(最大前缀后缀)
            i, j = i + 1, j + 1         # 向前行进
        else:
            i = pnext[i]                # 字符失配,模式串索引指针跳转至已记录最大匹配子串
    if i == pLen:                       # 模式串匹配完成
        return j - i                    # 返回主串中匹配字串首字符位置
    return -1                           # 匹配失败

紧接着,我们用图来说明,

以上,

文章不那么深入同时也不那么严谨,只是以简单的方式表述出作者对kmp算法的理解与拙见,希望能够帮助迷惑时的你。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值