由 D.E.Knuth、V.R.Pratt、J.H.Morris于1977年联合发表,简称KMP算法。
问题描述
由两个字符串 ms 和 s, 需要确认 s 是否存在于 ms 中,
如果存在,返回s的第一个字符在 ms的下标位置
朴素算法
使用双指针,i,j = 0, 分别指向 ms 和 s 的第一个字符,然后向后挨个对比
是否相等,若是遇到不相等的,则 i = 1, j = 0, 继续刚才的操作...
朴素算法缺陷与KMP算法优化思路
假设
ms: abazabcaed
s: abad
当 i = 3, j = 3 时
如果按照朴素的做法,得知 ms[3] != s[3] 后,会转而比较 ms[1] 和 s[0],
主串和模式串的指针都回溯了。
其实没有必要,因为在这个时候,我们已经知道了两个字符串前3个字符是匹配的,如下:
012 3 45678
aba z abcad
aba d
因为 s[0] != s[1] , 所以 => ms[1] != s[0], 因此
我们可以直接跳过以 ms[1], s[0] 为起始的比较。
另外,由 s[0] == s[2],所以 => s[0] == ms[2]
所以我们可以直接以 ms[2], s[0] 作为起始,开始新一轮的比较,
然而正因为已知 s[0] == ms[2], 可以直接当作比较过了,于是令 j = 1
如下:
012 3 45678
aba z abcad
a b ad
所以,i = 3 不变,只是 j 从 3 回溯到了 1,KMP的优化思路就是这样,
既避免了主串指针(i)的回溯,又跳过了模式串中一些不需要再比较的字符。
如果两个字符串是如下所示:
012 3 45678
abc z abcad
abc d
依然是 i=j=3 时,发现不匹配, 同样的:
s[0] != s[1], => ms[1] != s[0], => 跳过 ms[1], s[0] 作为起始的比较;
s[0] != s[2], => ms[2] != s[0], => 跳过 ms[2], s[0] 作为起始的比较;
所以我们直接进入 ms[3] 和 s[0] 作为起始的比较。
KMP引入
通过上面的例子,我们可以总结一下:
假设当前模式串在 j 处发现了不匹配, => s[0:j] 都是匹配的。
主串的指针(i) 不回溯,我们只考虑在新的一轮比较中,和此时ms[i]进行比较的 j=?
1、如果 s[0:j] 是如 abc 这样的,则说明下一个 j=1,2 都是不行的, j应该为0
2、如果 s[0:j] 是如 aba 这样的,则说明下一个 j 应该直接为 1;
3、如果 s[0:j] 是如 abac 这样的,则说明下一个 j=1,2,3 也都是不行的,j应该0
找规律,进一步总结:
模式串只有形如 aba、abjab 这样首尾有重合的,才可以跳过一些字符,假设这样重复的
字符数为 k, 即有 s[0:k] == s[j-k:j], 因此,新的一轮和ms[i]进行比较的 j = k。
所以关键就在于找 k 值,在 j 处发现不匹配时 k 值:
尝试手动找 k 值:
01234567
abaabcbc
j=0, => 第一个就不匹配,k = 0, i应该 +1, 从下面开始 i 都不变
j=1, => s[0:1] == ms[0:1], 这里字串只有1个字符,没有重合问题, k=0,
j=2, => s[0:2] == ms[0:2], 但没有重合, j = k = 0, i不变,
j=3, => a.a 重合,j = k = 1,
j=4, => a..a 重合,j = k = 1,
j=5, => ab.ab 重合,j = k = 2,
j=6, => 无重合,j = k = 0,
j=7, => 无重合,j = k = 0
所以,我们可以设计一个函数, 求 k 值,即 k = f(j)。
为了降低复杂度,可在匹配前,计算出所有 k,并缓存到 next 数组中
get_nexts 函数设计()
我们是需要找到每个j往左的字符串中(s[0:j+1]),重合的字符数。
定义 nexts 数组
方案一(自我实现):
1、已知nexts[0]=nexts[1] = 0, 因此,从 j = 2 开始。
2、一开始不知道有多少个重合,但是可能的最大重合个数是 (j + 1) // 2,
因此,需要先将 s[0:j+1] 拆解为两条字符串(得到 left_j, right_j),
让它们进行匹配,left_j: 作为模式串,right_j: 作为主串。
3、这个匹配过程中,依然可以应用kmp的思想,right_j 不回溯,
当发生失配时,left_j = nexts[left_j],
因为j是从底自上的, 所以每次失配都可以从 nexts 中拿到下一个 left_j (动态规划的子问题思想)。
4、最终当 right_j == j 时,重合数 = left_j + 1
def get_nexts1(s):
length = len(s)
nexts = [0] * length
for j in range(2, length):
left_j, right_j = 0, j // 2 + 1 if j % 2 else j // 2
while right_j < j:
if s[left_j] == s[right_j]:
if right_j + 1 < j:
left_j += 1
right_j += 1
elif left_j == 0:
right_j += 1
else:
left_j = nexts[left_j]
k = left_j + 1 if s[left_j] == s[j - 1] else 0
nexts[j] = k
return nexts
方案二(优化):
方案一中进行了太多重复,没有必要的工作, 如
j = 7: abcdefg 对半分,需要从 a 和 e 开始比较, f,g 都不匹配,最终得到 nexts[7] = 0
j = 8: abcdefgh 还是从 a 和 e 开始比较,最终也得到 nexts[8] = 0
所以,不需要对每个j单独去对半分求解,可是不对半分,怎么能知道,某个 j下,字符串中有几个重合
字符呢? 这其实已经又回到了最初的问题了, i 和 j都回溯了。 这里可以继续应用 kmp 思想。
让模式串应用kmp的思想,自己和自己匹配,需要求解的重合字符数,实际上就等于,失配时,next[j+1] + 1。
这里挺绕的,一层又一层的,来回顾一下:
主串(ms): abcabaaaabaaacac
模式串(s): abaabcbc
1、为了不像朴素那样 回溯主串指针 i, 通过 nexts[j] 获取下一个 j
2、我们知道 nexts[j] == "s[0:j+1]中前后重合字符数"
3、于是我们要得到s[0:j+1]中前后重合字符数
然后我们让模式串进行自我匹配
当 j + 1 处发生失配时,new_j = next[j+1]
于是得到 "s[0:j+1]中前后重合字符数" == new_j + 1
4、同样的,在自我匹配过程中,当 s[i] == s[j] 时,其实便说明了一点:
假设在 j + 1 处失配,那么 j + 1 就是我们要求的 重合字符数,
即 nexts[i+1] = j + 1
# 妈的,真 TM 绕...
def get_nexts2(s): # 自己匹配自己
length = len(s)
nexts = [0] * length
j, k = 1, 0
while j < length - 1:
if s[j] == s[k]: # 说明 倘若 j+1 处发现不匹配,s[0:k+1] 都是匹配的,
# 即,s[0:j+1] 中,s[0:k+1] 与 s[j-k:j+1] 是重合的
# 即,重合数是 k + 1
k += 1
j += 1
nexts[j] = k
elif k == 0:
j += 1
nexts[j] = k
else:
k = nexts[k]
return nexts
方案三(官方);
def get_nexts(s): # 自己匹配自己
length = len(s)
nexts = [0] * length
j, k = 0, -1
nexts[j] = k
while j < length - 1:
if k == -1 or s[j] == s[k]:
# k == -1 , 说明 s[j] != s[0]
# s[j] == s[k], 说明 倘若 j+1 处发现不匹配,s[0:k+1] 都是匹配的,
# 即,s[0:j+1] 中,s[0:k+1] 与 s[j-k:j+1] 是重合的
# 即,重合数是 k + 1
k += 1
j += 1
nexts[j] = k
else:
k = nexts[k]
return nexts
KMP匹配算法
def kmp_match(ms, s):
i, j, m_len, s_len = 0, 0, len(ms), len(s)
nexts = get_nexts(s)
while i < m_len:
if j == -1 or ms[i] == s[j]:
if j != s_len - 1:
i += 1
j += 1
else:
return i - j
else:
j = nexts[j]
return -1