用PYTHON实现KMP字符串匹配

本文只讨论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 【情况三】

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

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

  • 如果对应字符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,避免范围溢出
        r = 0 #记录连续值
        for i in range(1,len(dp)):#跳过0
            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为匹配文本
        text, match = s, m
        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-1: #当文本index仍在合理值内,便继续循环
            if s[index-dp_index] == m[0]:# s[index-dp_index]能准确指出当前匹配部分文本字符串的头号字符;如果其与匹配字符串的头号字符相等,则继续, 否则直接增加index去对照下一块文本
                while index <= l-1 and s[index] == m[dp_index]: #当文本index仍在合理值内 且 当前文本字符与对应匹配字符相等,进入循环
                    if dp_index == dp_l-1: # 如果dp_index达到了最大理论长度,说明匹配字符全部通过匹配,程序完成
                        return index - dp_index #程序完成,进行反馈,返回发生匹配时最开始的序列
                    index += 1 #如果还未匹配完毕,则增加index与dp_index,继续匹配
                    dp_index += 1
                else:
                    dp_part = dp[:dp_index] #当匹配出现不相等时,提取dp_part,其为目前已通过匹配的部分匹配字符串
                    dp_index = max(dp_part) #将dp_index重新定位到目前dp_part中的最大值,这是下一步要比较的dp_index
            else:
                index += 1 #因为两组首字符不等,直接增加index比较下一组。
        return -1 #原文中并没有出现匹配字符 返回-1

原文地址:https://www.cnblogs.com/phinza/p/11751114.html