本文只讨论KMP的实现,原理可从以下网站自行阅读理解,写的非常好

http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

阮一峰大神写的


理解后可知实现KMP最重要的是部分匹配表PML(Partial match list)的构建及调用

从上面的链接引用一下:

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

  - "A"的前缀和后缀都为空集,共有元素的长度为0;

  - "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

  - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

  - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

  - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

  - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

  - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

 

PML表的功能应包含:

标出对应单个字符所在位置的最大部分匹配值PMV(Partial match value)

 

本质上就是动态规划的实现,因此设一个DP表,每一个位置代表对应INDEX字符的部分匹配值

构建思路:

DP[i]的值实际上与DP[i-1]的值有关联,匹配时可能出现下面几种情况:

1.如果DP[i-1]存在值

说明与前面的字符串已经开始匹配了,

  • 如果对应字符s[i]等于下一个应该轮到匹配的前面的字符,则PMV继续增加一个单位

  如:将ABCAB 中的第二个B与第一个B进行匹配。

  则DP[i] = DP[i-1]+1 【情况一】

  • 如果对应字符s[1]不等于下一个应该轮到匹配的前面的字符,此时又有情况分支:

    1.如果s[i]等于字符串的第一个字符,则又可以在此开始新的一轮匹配,

     如:ABCAA,在这里第三个A不等于B,但是它等于第一个A,所以DP[i]仍为1 【情况二】

    2.如果s[i]不等于字符串的第一个字符,那DP[i]就为0 【情况三】

    3.如果s[i]与往前【连续匹配次数】格的字符串连续相等,那DP[i]就为DP[i-1] 【情况四】如AABAAAAA的匹配值为01012222

2.如果DP[i-1]不存在值

说明之前一个单位也没有匹配的情况,

  • 如果对应字符s[i]等于字符串第一个字符,则DP[i] = 1,在此开始新的匹配 【情况五】
  • 如果对应字符s[i]不等于字符串第一个字符,则DP[i] = 0 【情况六】

仔细一看,【情况二三】和【情况五六】其实可以放在一起讨论,【情况四】独立:

【情况一】的实现条件整合一下:

条件一:DP[i-1]存在值

条件二:s[i] 等于轮到匹配的前字符

实现上述两个条件则DP[i] = DP[i-1]+1

【情况四】的实现条件:

条件一: DP[i-1]大于1

条件二:往前【连续匹配次数】格的字符串连续相等

实现上述两个条件则DP[i] = DP[i-1]

不符合上述条件的直接分类到【情况二三五六】:

  • 如果对应字符s[i]等于字符串第一个字符,则DP[i] = 1,在此开始新的匹配 【情况二/四】
  • 如果对应字符s[i]不等于字符串第一个字符,则DP[i] = 0 【情况三/五】

上PML的构建代码:

def Partial_match_list(lst): #建立部分匹配表
        dp = [-1] + [0 for i in range(len(lst))] #留个-1,避免范围溢出

   lst = '_'+lst #个人习惯,为了dp与lst下标相等
        r = 1 #记录连续值
        for i in range(1,len(dp)):#跳过0
            if lst[i] == lst[r] and dp[i-1]: #判断1:上一个DP点存在值 #判断2:str当前值与记录值相同
                dp[i] = dp[i-1]+1 #条件通过:当前dp值为上个dp值+1
                r += 1 #连续记录值+1
            elif dp[i-1] > 1 and len(set(lst[1:i+1][-r:])) == 1: #判断1:上一个DP点值大于1(连续两个以上记录),判断2:往前【连续匹配次数】格的字符串连续相等
                dp[i] = dp[i-1] #条件通过:当前dp值与上一个dp值相等,但是r不增加,因为连续记录数不变
            else:#如果不符合上述条件
                dp[i] = 1 if lst[i] == lst[1] else 0 #如果str当前值与首值相等,则当前dp为1,否则为0
                r = 2 if dp[i] else 1 #如果当前dp为1,则记录值也为2,否则为1


            if dp[i-1] and lst[i-1] == lst[r]: #判断1:如果上一个点存在值 #判断2:如果str当前值与记录值相同
                dp[i] = dp[i-1]+1 #条件通过:当前dp值为上个dp值+1
                r = dp[i] #记录值与DP值相同
            else:#如果不符合上述条件  
                dp[i] = 1 if lst[i-1] == lst[0] else 0 #如果str当前值与首值相等,则当前dp为1,否则为0
                r = 1 if dp[i] else 0 #如果当前dp为1,则记录值也为1,否则为0
        return dp[1:] #由于0号是建表初期拿来避免范围溢出的,后续用不到,所以不输出


 

有了PML后,KMP的实现就相对简单很多了

def KMP(text, match): #text为原文,match为匹配文本
        s, m = text, match
        if len(m) > len(s): #如果查询字符大于文本,直接退出
            return -1
        dp = Partial_match_list(m) #使用先前代码制作PML
        index,dp_index = 0,0 #设定文本index与dp_index:文本index用于标注检测中的文本字符位置;dp_index用于标注检测中的匹配字符位置
        l,dp_l = len(s), len(dp) #计算文本长度与dp长度:文本长度用于检测循环是否到末尾;dp长度用于检测是否达成检索目标
        while index < l: #当文本index仍在合理值内,便继续循环
                if s[index] == m[dp_index]: #情况一:当前文本字符与对应匹配字符相等
                    if dp_index == dp_l-1: # 如果dp_index达到了最大理论长度,说明匹配字符全部通过匹配,程序完成
                        return index - dp_index #程序完成,进行反馈,返回发生匹配时最开始的序列
                    index += 1 #如果还未匹配完毕,则增加index与dp_index,继续匹配
                    dp_index += 1
                elif s[index] != m[dp_index] and dp_index:#情况二:当前文本字符与对应匹配字符不相等;但是前段已经匹配了一部分字符

      dp_index = dp[dp_index-1] #将dp_index重新定位到目前dp_part中最后一个值,这是下一步要比较的dp_index——KMP的关键思路,相当于前面dp_index的字符已经通过匹配,直接从+1的位置和原文index位置进行匹配。

      else: #情况三:当前文本字符与对应匹配字符不相等;且前端无匹配字符
                  index += 1 #直接增加index比较下一组。
        return -1 #原文中并没有出现匹配字符 返回-1

 

        for i in range(1,len(dp)):
            if lst[i] == lst[r] and dp[i-1]:
                dp[i] = dp[i-1]+1
                r += 1
            elif dp[i-1] > 1 and len(set(lst[1:i+1][-r:])) == 1:
                dp[i] = dp[i-1]
            else:
                dp[i] = 1 if lst[i] == lst[1] else 0
                r = 2 if dp[i] else 1