本文只讨论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