KMP算法小结

KMP算法

很早以前就了解到有KMP算法的存在,当时就是知道其可以高效的匹配字符串,但是没敢细看(其实看了一眼,又吓得赶紧退出来。。),最近突然又看到这个算法,就想着学习一下,鉴于自己理解力不太够,花了好久才明白个大概,因此防止过两天就忘,在这里做个笔记。

字符串前缀与后缀

  • 前缀:除了最后一个字符以外,一个字符串的全部头部组合。
  • 后缀:除了第一个字符以外,一个字符串的全部尾部组合。
  • 最大前后缀:前缀集与后缀集交集中的最大长度的子串

如下图所示:

KMP算法思想

在匹配的过程中,如文本串S匹配到i,模式串匹配P到位置j时,两者不匹配,即S[i]!=P[j]时,保持i不变,j移动到next[j]=k位置处,k等于P[j]之前子串的最大前后缀的长度。相当于将模式串P向后移动j-next[j]个位置。

与暴力破解法相比,其文本串S指针不会回溯。
暴力匹配代码(python):

def baoli(s,t):
    s_len=len(s)   #目标字符串
    t_len=len(t)   #匹配字符串
    i=0
    j=0
    while i<s_len and j <t_len:
        if s[i]==t[j]:
            i+=1
            j+=1
        else:
            j=0
            i=i-j+1
    if j==t_len:
        return i-j
    else:
        return -1
if __name__=='__main__':
    s='sdfwowjeofijowejifeowji'
    t='feo'
    print(baoli(s,t))

KMP算法复杂度

KMP算法是指在一个文本串S内查找一个模式串P 的出现位置,假设S的长度为n,P的长度为m,传统的暴力匹配法时间复杂度为O(m*n),而KMP算法由两个部分构成:

  • 求模式串P的next数组:时间复杂度为O(m)
  • 根据next数组在文本串S中匹配P:时间复杂度为O(n)

因此,KMP算法的时间复杂度为O(m+n)

KMP算法过程

前面提到,KMP算法由两个过程组成,首先第二个过程比较好理解。

  • 根据next数组在文本串S中匹配P,这里先给出python代码:
def kmp(s,t):
    s_len=len(s)   #目标字符串
    t_len=len(t)   #匹配字符串
    i=0
    j=0
    next_array=get_nextArray(t)
    while i<s_len and j<t_len:
        if j==-1 or s[i]==t[j]:  #j==-1存在于开头第一个字符就没匹配上,此时next[j]=-1,因此i+=1后目标字符串向右移一位等于0,
            j+=1                #表示匹配字符串继续从第一个开始匹配
            i+=1
        else:
            j=next_array[j]
    if j==t_len:
        return i-j
    else:
        return -1

KMP算法核心在于第一部分:求next数组。它是通过递推的方式来解决:已知next [0, …, j],如何求出next [j + 1]??分为两种情况:

  • 若p[k] == p[j],则next[j+1] = next [j]+1 = k+1;
  • 若p[k] ≠ p[j],如果此时p[next[k]] == p[j],则next[j+1] = next[k]+1,否则继续递归前缀索引k = next[k],而后重复此过程。
关键:为什么递归k=next[k],当p[k]=p[j]时就可以得到next[j+1]=k+1

我在这个地方纠结了很久,不知其所以然心里总是不得劲。。相信很多人跟我一样,因此就自己举例子,通过例子来理解会更清楚。其实这就是next数组的特性,这里的递归:k = next[k]与第一部分kmp算法匹配中的:j=next_array[j]原理是一样的,前者是与模式串P的p[j]比较来决定是否递归,后者是与文本串S中的S[i]对比

如上图所示,求取next[j+1]时,已知next[:j],且next[j]=4=k,此时根据递推式,因为p[j]=’c’,p[k]=p[4]=’a’,因此令k=next[k]=1,此时p[k]=p[1]=’c’=p[j],因此next[j+1]=k+1=2。

通过上述例子可以看到,当p[k]=p[1]=’c’与p[j]相等时候,其前面的k个字符必然等于p[4]=’a’前面的k个字符,同样等于p[j]前面k个字符,因此得到next[j+1]=k+1

第二部分j=next_array[j]原理也是一样,只是它不是跟p[j]比较,而是与文本串S[i]比较。都是在最长前后缀子串中寻找最长子串,直到匹配为止。

next数组求取过程(python):

def get_nextArray(t):
    next_array=[-1]
    t_len=len(t)
    k=-1   #因为采用的是从前往后递推的方法,因此需要知道初始状态:j=0时,next[0]=k=-1
    j=0
    while j<t_len-1:   #之所以是len-1:next[0]是固定值为-1
        if k==-1 or t[j]==t[k]:
            k+=1
            j+=1
            next_array.append(k)
        else:
            k=next_array[k]
    return next_array

然而上述next数组存在一定的问题,以下图为例,若在i=5时匹配失败,按照3.2的代码,此时应该把i=1处的字符拿过来继续比较,但是这两个位置的字符是一样的,都是B,既然一样,拿过来比较不就是无用功了么?之所以会这样是因为KMP不够完美。因此需要对代码进行一定的修改,如下所示:

  • 如果a位字符与它的next值(即next[a])指向的b位字符相等(即p[a] == p[next[a]]),则a位的next值就指向b位的next值即(next[ next[a] ])。

这里写图片描述

next数组(优化后)求取过程(python):

def get_nextArray_inp(t):  #改进的next数组,当k=next[j],且p[j]=p[k]时,即next[j]=next[next[j]]=next[k]
    next_array=[-1]
    t_len=len(t)
    k=-1
    j=0
    while j<t_len-1:
        if k==-1 or t[j]==t[k]:
            k+=1
            j+=1
            if t[j]==t[k]:
                next_array.append(next_array[k])
            else:
                next_array.append(k)
        else:
            k=next_array[k]
    return next_array

KMP算法优化版与未优化版在某些情况下区别很大,如下所示:
  KMP算法(未优化版): next数组表示最长的相同前后缀的长度,我们不仅可以利用next来解决模式串的匹配问题,也可以用来解决类似字符串重复问题等等,这类问题大家可以在各大OJ找到,这里不作过多表述。
  KMP算法(优化版): 根据代码很容易知道,优化后的next仅仅表示相同前后缀的长度,但不一定是最长(我个人称之为“最优相同前后缀”)。此时我们利用优化后的next可以在模式串匹配问题中以更快的速度得到我们的答案(相较于未优化版),但是上述所说的字符串重复问题,优化版本则束手无策。
  所以,该采用哪个版本,取决于你在现实中遇到的实际问题。

关于KMP算法的博客介绍很多,写的都特别详细,本文就不重复介绍了,主要插入图片和排版太麻烦了。。详细见下面的参考部分

参考:

  1. https://segmentfault.com/a/1190000008575379
  2. https://www.cnblogs.com/cherryljr/p/6519748.html
  3. http://www.cnblogs.com/zhangtianq/p/5839909.html

猜你喜欢

转载自blog.csdn.net/lzq20115395/article/details/79783044