kmp算法总结
1. 什么是kmp算法
现有两字符串a, b, 我们需要统计后者在前者中出现的次数,kmp算法就是用来解决此类问题的
2. 暴力
显然两层for循环可以暴力求解该问题,时间复杂度为 O ( n ∗ m ) O(n * m) O(n∗m), n和m分别为两字符串的长度
令 s = “ABADABACA”, p = “ABA”, s称为主串, p称为模式串
暴力的做法就是枚举匹配的起点,效率较低,这时候需要kmp算法
3,kmp算法
现在来考虑如何优化暴力的算法,判断字符串是否相等的时间复杂度是没法改变的,只能一个一个字符的判断,所以我们只能设法减少判断的趟数
暂且忽略最下面的nxt数组,此时两字符串的前三个字符相同,暴力做法是将p后移一位,而如果是kmp算法,则会将p后移两位(显然这种做法更符合人的直觉)
考虑什么时候我们可以让p直接后移几步而为无脑每次只后移一步。如果我们每次移动之后,不是从p的首个字符开始匹配,而是尽可能从p[0]之后的字符开始,这就要求p[0:i]已经匹配成功。
要实现这样的效果,需要对模式串进行预处理,引入nxt数组,定义nxt[i]表示p[0]—p[i]的这段子串的最长相同前后缀的长度:
前缀:起点为p[0],不包括p[-1]的子串,即’A’, ‘AB’
后缀:终点为p[-1], 不包含p[0]的子串, 即’BA’, ‘A’
nxt[0]: p[0]无前后缀,所以nxt[0] = 0
nxt[1]: p[0]-p[1]无相同前后缀,所以nxt[1] = 0
nxt[2]; p[0]-p[2], 相同且最长前后缀为’A’, 所以nxt[2] = 1
当s[i] != p[j] 时,就可以利用nxt数组,将j = nxt[j - 1], 就不需要从头开始匹配,而是利用相同前后缀,跳过相同的部分
例如,i = 2 时,完成了一次匹配工作,暴力的做法是将 i 回调为 1, 重头开始扫描整个p, 当时kmp的做法是将 i = 3, j = nxt[2] = 1, i的数值只会增加, j 尽量不回调到p的起始位置
4.代码
def kmp(s, p):
# 建立nxt数组
nxt = [0]
plen = 0 #当前已扫描的p子串的最长相同前后缀长度
i = 1
while i < len(p):
if p[plen] == p[i]:
plen += 1
nxt.append(plen)
i += 1
else:
if plen == 0: # 当前子串无相同前后缀
nxt.append(0)
i += 1
else:
plen = nxt[plen - 1]
# 利用nxt数组进行匹配
i, j, cnt = 0, 0, 0
while i < len(s):
if s[i] == p[j]:
i += 1
j += 1
else:
if j == 0: # j 回调到起始位置,还无法匹配
i += 1
else:
j = nxt[j - 1] # j 回调
if j == len(p): # 匹配成功
cnt += 1
j = nxt[j - 1] # 这里记得回调j,否则p的下标越界
return cnt
5. 时间复杂度分析
时间复杂度分析:
- 建立nxt数组,扫描一次p串,回调j的次数可以忽略
- 利用nxt匹配,扫描一次s串,回调j的次数可以忽略
故kmp算法的时间复杂度为 O ( m + n ) O(m + n) O(m+n)