记录对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算法的理解与拙见,希望能够帮助迷惑时的你。