PS:KMP算法的背景相关请自行百度,本篇编程语言使用python,个人学习用,理解上可能存在谬误。算法实现出自本人自以为理解的KMP而码成,不保证正确性以及性能最优。
假设有两个字符串s, r,求r在s中出现的位置,效率最高的算法似乎就是KMP了。
1.生成next数组
算法的第一步是生成r的next数组,next[i],其中0<=i<len(r),的值即为子串r[:i+1]的最大的相同前后缀长度减去1。换一种理解,next[i]即为子串r[:i+1]中最大的相同前后缀中最大前缀的末尾的索引。
用一个例子说明,若r = "ababc"
next[0],即子串"a"的最大的相同前后缀长度显然是0.因为"a"的长度为1,是不可能有前缀或后缀的,故值为-1
next[1],即子串“ab”,同样,也不存在相同的前后缀,值为-1
next[2],即子串"aba",此时,最大的相同前后缀显然就是"a",所以next[i]为子串“aba”中最大的相同前后缀中最大前缀“a”的末尾的索引,即0.
next[3],即子串"abab",此时,满足条件最大前缀为"ab",那么next[3]的值为1
next[4],即子串"ababc",此时显然无满足条件的相同前后缀,值为-1
所以,"ababc"的next数组为:[-1, -1, 0, 1, -1]
那么,如何用程序去计算出这个next数组呢?
这里面,个人觉得KMP算法中最大的一个亮点,也是算法设计灵感的源泉,来源于这么一个现象:
假设我们已经有了0...i的next值,那么,现在要求next[i+1]的值,
我们先获取next[i]的值,这个值为子串r[:i+1]的最大的相同前后缀中最大前缀的末尾的索引,记为k.
那么r[:k+1] 就等于 r[i-k:i+1],
(*)此时我们考察r[i+1]是否等于r[k+1]:
如果两者相等,那么对于子串r[:i+2]来说,其最大的相同的前后缀即为r[:k+2],故而next[i+1]的值就是k+1
如果两者不等,这里面最有意思的是,我们取k = next[k],然后继续比较r[i+1]是否等于r[k+1], 回到(*)处循环了起来。
结合上述例子,看下i=2的时候,此时next[2] = 0, 现在求next[3]的值,
那么k = next[2], 即k= 0,此时考察r[i+1]是否等于r[k+1],即r[3]是否等于r[1],这里两者都是"b",相等,因此next[3] = k + 1,即1.
对照实例,继续理解的话,这个算法理解起来就不难了。
这里贴出我理解后的python实现:
def gen_next(r):
m = len(r)
if m == 1:
return [-1]
next = [-1 for _ in range(m)]
k = -1
for i in range(1, m):
no_match = False
while r[k +1] != r[i]:
if k == -1:
next[i] = -1
no_match = True
break
k = next[k]
if no_match:
continue
k += 1
next[i] = k
return next
print(gen_next("ababaca"))
2.KMP算法主体
其实能够手撸next出来的,也很容易就能把这个搜索函数给码出来。
对于从字符串l中搜索子串r, 假设我们在此场景:
当前搜索到l的i位置,即目前扫描到字符l[i],对于r,已经有(k+1)个字符匹配,当前正在扫描r[k+1],并且此时必然有:
r[:k+1]等于l[i-k-1:i]。那么,此时考察l[i]是否等于r[k+1],
(*)若l[i] == r[k+1]:
说明串进一步匹配,i和k更进一步
若不等:
此时取k = next[k],回到(*)处继续判断。
这里又出现了k = next[k],这应该就是KMP的灵魂所在了。其实光看文字描述感觉很抽象,但是结合实际例子,便很直观了
这里画了张示意图,其中红色部分为相同串。
接下来是最后的python实现:
def gen_next(r):
m = len(r)
if m == 1:
return [-1]
next = [-1 for _ in range(m)]
k = -1
for i in range(1, m):
no_match = False
while r[k +1] != r[i]:
if k == -1:
next[i] = -1
no_match = True
break
k = next[k]
if no_match:
continue
k += 1
next[i] = k
return next
def kmp(l, r):
l_len = len(l)
r_len = len(r)
if l_len == 0 or r_len == 0:
return
next = gen_next(r)
k = -1
for i in range(l_len):
no_match = False
while l[i] != r[k + 1]:
if k == -1:
no_match = True
break
k = next[k]
if no_match:
continue
k += 1
if k == r_len - 1:
return i - k
print(kmp("bacbababadababacambabacaddababacasdsd", "ababaca"))
后记:根据算法去理解其意图是比较简单的,但是从无到有创造出KMP算法的人真她娘的是个天才。