其他算法-KMP

KMP算法

KMP算法由D.E.Knuth,J.H.Morris和V.R.Pratt提出,故得名KMP,用于模式字符串和原始字符串的匹配;
匹配的描述:给定文本text和模式字符串pattern,从text中找出pattern第一次出现的位置;
首先想到暴力求解(Brute Force),假设text的字符数量是n,pattern的字符数量是m,规定i是pattern的首字符在text中的匹配位置(i属于0到n),j是目前检测到模式字符串的匹配位置(j属于0到m);
n个字符都与m个字符分别比较一次是否相等,即如果 t e x t [ i + j ] = p a t t e r n [ j ] text\left [ i+j \right ]=pattern\left [ j \right ] text[i+j]=pattern[j],则j向后移动一位进行两字符串的下一字符比较,如果 t e x t [ i + j ] ≠ p a t t e r n [ j ] text\left [ i+j \right ]\neq pattern\left [ j \right ] text[i+j]=pattern[j],则i先后移动一位,j复位到0,进行下一个text字符的比较;
可见时间复杂度是O(mn)的;
fig1

暴力求解的实现如下:

#暴力求解
def bruteforce(text,pattern):
    tsize=len(text)
    psize=len(pattern)
    i=0#当前模式首字符在text中的匹配位置
    j=0#模式字符串的匹配位置
    while i<tsize and j<psize:
        if text[i+j]==pattern[j]:
            j+=1
        else:
            i+=1
            j=0
    if j>=psize:
        return i
    return None
bruteforce('python','th')

显然,这是很耗费时间的,因为每次匹配失败,pattern的j立刻就复位到pattern的首位(即j=0),如果pattern的开头几个连续字符和中间的几个连续字符相同,明显可以不用直接复位到j=0,这样就能减少多余的判断;


重点
可以想一下,pattern和text比较时,pattern的第j位之前有三个子串构成[str1,str2,str3],而str1和str3是一样的,既然已经比较到第j个字符,意味着[str1,str2,str3]都已经匹配对了,而在遇到第j个字符时,偏偏不相等,j可以不用回到pattern[0]处,由于str1和str3是一样的,此时text的i可以不变,j回到str1的下一位置处,即当前text的i与pattern的str2首位比较,因为在text的i前已经确定str3是匹配对的,即str1也是匹配对的;所以j只需要回溯到pattern里str1的下一位置;


计算next数组

KMP的要点就在于计算了一个数组,该数组可以确定j应该回到pattern哪个位置,在KMP里,这个数组叫做next数组;
计算next数组:
在pattern[j]前的子串(pattern的j-1前缀):
p a t t e r n j − 1 = p 0 p 1 . . . p j − 1 pattern_{j-1}=p_{0}p_{1}...p_{j-1} patternj1=p0p1...pj1
在其中找最长前缀和后缀,假设最长为k,则前缀为 p 0 p 1 . . . p k − 1 p_{0}p_{1}...p_{k-1} p0p1...pk1,后缀为 p j − k p j − k + 1 . . . p j − 1 p_{j-k}p_{j-k+1}...p_{j-1} pjkpjk+1...pj1;它们关系应该是:
p 0 p 1 . . . p k − 1 = p j − k p j − k + 1 . . . p j − 1 p_{0}p_{1}...p_{k-1}=p_{j-k}p_{j-k+1}...p_{j-1} p0p1...pk1=pjkpjk+1...pj1
从而计入next[j]=k,可见,next数组只与pattern有关,给出pattern就能计算出对应的next数组;
举例:对于pattern:“abaabcaba”,当j=5,即在c位置,最大前缀和后缀是"ab",所以next[5]=2,当pattern与text比较到c发现不匹配时(发现text[i]不是c),则根据next[5]=2,回溯到pattern[2]的a与text[i]再次比较;
这样人为的求next是不现实的,所以引出了next的递推关系:
对于 j j j n e x t [ j ] = k next[j]=k next[j]=k,即: p 0 p 1 . . . p k − 1 = p j − k p j − k + 1 . . . p j − 1 p_{0}p_{1}...p_{k-1}=p_{j-k}p_{j-k+1}...p_{j-1} p0p1...pk1=pjkpjk+1...pj1,对于 j + 1 j+1 j+1,需要看pattern的j前缀:
fig2

p [ k ] = p [ j ] p[k]=p[j] p[k]=p[j],则 n e x t [ j + 1 ] = n e x t [ j ] + 1 next[j+1]=next[j]+1 next[j+1]=next[j]+1
而如果 p [ k ] ≠ p [ j ] p[k] \neq p[j] p[k]=p[j],则记录 k = n e x t [ k ] k=next[k] k=next[k],若 p [ k ] = p [ j ] p[k]=p[j] p[k]=p[j],则 n e x t [ j + 1 ] = k + 1 next[j+1]=k+1 next[j+1]=k+1;不相等的话,就再重复该过程;
计算方法的实现如下:

"""
next是以首个字符为研究对象
递推关系:
next[0]=-1
next[j]=k,即p[0到k-1]=p[j-k到j-1]
对于j+1:
若p[k]=p[j],则next[j+1]=next[j]+1

若p[k]!=p[j],记录next[k],若p[next[k]]=p[j]
"""
def calcnext(pattern):
    """
    该函数的实现与理论差异在于next[j]的计算对象是pattern[0->j]
    """
    #k=0代表无匹配前缀后缀
    k, psize = 0, len(pattern)
    pnext = [0]*psize
    j = 1
    while j < psize:
        if (pattern[j] == pattern[k]):
            pnext[j] = k + 1
            k += 1
            j += 1
        elif (k!=0):
            k = pnext[k-1]
        else:
            pnext[j] = 0
            j += 1
    #现在pnext[0, 0, 1, 1, 2, 0, 1, 2, 3]
    #处理一下变回理论习惯:next[j]的计算对象是pattern[0->j-1]
    pnext.insert(0,-1)
    pnext.pop()
    #[-1, 0, 0, 1, 1, 2, 0, 1, 2]
    return pnext

nextarr=calcnext('abaabcaba')
nextarr

#[-1, 0, 0, 1, 1, 2, 0, 1, 2]

KMP实现

基于next数组,实现KMP就是一个简单的过程:
首先注意,在索引i上,与暴力求解时相比,做一点改动,BF中是 t e x t [ i + j ] text[i+j] text[i+j] p a t t e r n [ j ] pattern[j] pattern[j]比较,现在让 i i i代表BF中的 i + j i+j i+j,即 i i i就是text的字符下标,这样改动利于计算出第一次匹配的位置;
如果 t e x t [ i ] = p a t t e r n [ j ] text[i]=pattern[j] text[i]=pattern[j],则i和j都向后移动一位;
如果 t e x t [ i ] ≠ p a t t e r n [ j ] text[i] \neq pattern[j] text[i]=pattern[j],则i不变, j = n e x t [ j ] j=next[j] j=next[j],再比较,当 j = p a t t e r n s i z e j=pattern size j=patternsize时,代表匹配结束,则首次匹配正确的首个字符位于 i − p a t t e r n s i z e i-patternsize ipatternsize,实现如下:

def kmp(text,pattern,nextarr):    
    ans=-1
    i=0
    j=0
    psize=len(pattern)
    textsize=len(text)
    
    while i<textsize:
        #j=-1代表已回溯到pattern[0],text移位,pattern复位至pattern[0]
        if j==-1 or text[i]==pattern[j]:
            i+=1
            j+=1
        else:
            j=nextarr[j]
        
        #看是否匹配结束
        if j==psize:
            ans=i-psize
            break
    return ans

nextarr=calcnext('aabc')
#从0开始计数,aabc在text[7]首次出现
kmp('pythonaaabcdpython','aabc',nextarr)
#7
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值