KMP算法介绍
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
暴力解法
目标串:ABACABAB
模式串:ABAB
利用表格的形式,阐释暴力解法的思路,每次取目标串的一个字符与模式串的字符进行对一一对比,只要相等,则比较下一位。
若不相等,则从目标串的下一次位置开始。
目标串 | A | B | A | C | A | B | A | B | |
第一次 | A | B | A |
| | | | | × |
第二次 | | A | B | A |
| | | | × |
第三次 | | | A | B | A |
| | | × |
第四次 | | | | A | B | A |
| | × |
第五次 | | | | | A | B | A | B | √ |
代码如下,最容易让人理解的思路。但是空间复杂度为O(m*n).
def Brute_Force(target_str, model_str):
"""
:param target_str:目标串
:param model_str: 模式串
:return: 首次匹配位置
"""
target_len = len(target_str)
model_len = len(model_str)
# 循环次数为目标串长度-模式串长度
for i in range(target_len - model_len + 1):
# 变量K的作用,遍历模式串
k = 0
for j in range(i, i + model_len):
# print("i", i, "j", j, "target_str", target_str[j], "model_str", model_str[k])
# 不匹配
if target_str[j] != model_str[k]:
break
else:
k += 1
else:
return i
改变版,匹配,减少循环次数,只使用一次循环。
def Brute_Force_upgrade(target_str, model_str):
target_len = len(target_str)
model_len = len(model_str)
# 循环用上面代码中循环一样
for i in range(target_len - model_len + 1):
# 比较字符时候,取同模式串长度一样的目标串
# 满足输出首次匹配位置
if target_str[i:i + model_len] == model_str:
return i
普通求解的方式,思路容易理解,但是循环比较的次数比较多,每次都是从本次目标串循环的下一次位置,从新开始,这样会浪费许多的循环比较。
目标串:ABABBBAAABABABBA
模式串:ABABABB
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | |
主串 | A | B | A | B | B | B | A | A | A | B | A | B | A | B | B | A | T/F |
1 | A | B | A | B | A | | | | | | | | | | | | × |
2 | | A | | | | | | | | | | | | | | | × |
3 | | | A | B | A | | | | | | | | | | | | × |
4 | | | | | | A | | | | | | | | | | | × |
5 | | | | | | | A | B | | | | | | | | | × |
6 | | | | | | | | | A | B | A | B | A | B | B | | √ |
分析此目标串和模式串:
第一次比较:比较到第五个位置,出错
第二次比较:比较到第二个位置,出错
第三次比较:比较到第五个位置,出错
......
第六次比较:成功匹配
仔细比较目标串的第五个元素的前面四个元素,ABAB,有重叠的部分。并且都是在目标串的第五个位置匹配失败。
因此前边的几次比较都是无效的比较。
故,我们想优化比较的次数。
引入next数组。
接下来讲解KMP算法
KMP算法的重点是求解next数组。
next数组手动求解
next数组是在模式串上寻找的一个数组,先给出模式串ABABABB的next数组。接下来详细介绍求解过程。
下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
模式串 | A | B | A | B | A | B | B |
next[t] | 0 | 1 | 1 | 2 | 3 | 4 | 5 |
next数组就是为了解决,目标串指针移动问题而引入的。
next数组,寻找模式串当前位置的重复子串。
不做过多的讲解,为什么要引入这个next数组,用最简单的方法,让你们学会怎么求解next数组。
比较子串中左右两部分最长的重叠部分
(左部分记为:L【忽略子串的最后一个位置】, 右部分记为:R【忽略子串的第一个位置】)
当下标=1时,初始化next数组,next[0]=0。
当下标=2时,对应的子串为A,L=null,R=null,因此没有重复的串内容(本身重复不属于),故长度L=0,next[2]=L+1=1。
当下标=3时,对应的子串未AB,L=A,R=B,因此没有重复的串内容,故长度L=0,next对应next[3]=L+1=1。
当下标=4时,对应的子串未ABA,L=AB,R=BA,重复子串为两端的A,故长度L=1,next对应next[3]=L+1=2。
当下标=5时,对应的子串未ABAB,L=ABA,R=BAB,重复子串为两端的AB,故长度L=2,next对应next[3]=L+1=3。
当下标=6时,对应的子串未ABABA,L=ABAB,R=BABA,重复子串为两端的ABA,故长度L=3,next对应next[3]=L+1=4。
当下标=7时,对应的子串未ABABAB,L=ABABA,R=BABAB,重复子串为两端的ABAB(本身重复不符合要求),故长度L=4,next对应next[3]=L+1=5。
在来一个例子,加深理解。求解方式同上一样。
模式串:ABABBBAA
下标 i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
模式串 | A | B | A | B | B | B | A | A |
next[t] | 0 | 1 | 1 | 2 | 3 | 1 | 1 | 2 |
next数组代码求解
根据手动求解的思路,每次比较当前下标元素与t
def getNext(substrT):
next_list = [0 for i in range(len(substrT))]
# 初始化下标,j指向模式串
# t记录位置,便于next数组赋值
j = 1
t = 0
while j < len(substrT):
# 第一次比较 t等于0 直接进行赋值,每次长度自增1
# 之后进行的比较 判断字符是否相等, python数组下标从0开始,因为均-1
if t == 0 or substrT[j - 1] == substrT[t - 1]:
# 长度+1
next_list[j] = t + 1
j += 1
t += 1
else:
# 此时的-1 同上边的-1操作是一样
t = next_list[t - 1]
return next_list
理解代码最好的办法,就是手动的执行一遍这个流程。
kmp算法的核心,next数组已经求解出来,只要在优化我们之前写的暴力解法,减少比较的次数。
def KMP(target_str, model_str):
next = getNext(model_str)
# 主串计数
i = 0
# 模式串计数
j = 0
while (i < len(target_str) and j < len(model_str)):
if (target_str[i] == model_str[j]):
i += 1
j += 1
elif j != 0:
# 调用next数组
j = next[j - 1]
else:
# 不匹配主串指针后移
i += 1
if j == len(model_str):
return i - len(model_str)
else:
return -1
重点在于next数组的求解,手动按照代码流程,写几个next数组,肯定有助于加深对KMP算法的理解。
在求解next数据的时候 还要一些瑕疵,等待后续更新。
模式串:AAAAAAB
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
模式串 | A | A | A | A | A | A | B |
next[j] | 0 | 1 | 2 | 3 | 4 | 5 | 6 |