说道KMP算法,一定要先指出问题所在之处了,问题如下:
已知一个字符串a,b
想找到在字符串a中b首次完整出现的位置。
听到这个问题,首先想到的就是朴素算法,挨个来进行匹配,然后找到对应的答案。
朴素算法
朴素算法很简单,就是挨着遍历,但是会有很多的重复计算,算法复杂度为O(mn)
代码如下:
# 朴素算法
def algorithm1(src, target):
for i in range(len(src)):
flag = True
for j in range(len(target)):
if i + j >= len(src):
return -1
if src[i + j] != target[j]:
flag = False
break
if flag:
return i
return -1
考虑如下字符串,重复的计算就很多了,因为直到最后才最终匹配。
src:abbacabbacabbacd
target:abbacd
如果匹配如上两个字符串,就会出现大量的重复计算,所以才有了之后的优化算法
KMP算法
KMP算法跟朴素算法是对朴素算法的一个优化,朴素算法很简单,但是会有大量的重复匹配,而KMP算法就是要把重复的匹配去掉。
KMP算法有个核心的概念,就是next数组,该数组记录最长共有元素的长度。
以abcab为例,其next = [0,0,0,1,2]
说起来比较难以理解,就是部分匹配的值。
算法思想
部分匹配的实质就是其实也是KMP算法的核心思想,KMP算法的核心思想在我看来就是减少匹配的次数,将重复的匹配次数去掉,那这个信息该如何去掉呢?
我们使用两个标志位,一个为主字符串的标志位,一个是模式串的标志位。
代码如下:
# KMP 算法
def algorithm2(src, target):
l = make_next(target)
target_length = len(target)
src_length = len(src)
src_index = 0
target_index = 0
while src_index < src_length:
while target_index < target_length:
if src_index + target_index >= src_length:
return -1
if src[src_index + target_index] == target[target_index]:
target_index += 1
elif target_index >= 1:
src_index += target_index - l[target_index - 1]
target_index = l[target_index - 1]
else:
src_index += 1
target_index = 0
if target_index == target_length:
return src_index
return -1
先不看next数组的计算方法,直观的来看KMP算法的话,我们就能理解其思想了,KMP算法虽然是2层循环叠加,但是本质上,src_index 和 target_index 都是只进行了单次遍历的,所以算法复杂度为O(m + n)
比朴素算法的O(mn)快了很多。
了解了算法思想就该了解next数组的计算原理了
next数组
看如下代码
# next 数组的计算
def make_next(target):
l = [0] * len(target)
flag = 0
for i in range(1, len(target)):
if target[i] == target[flag]:
flag += 1
else:
flag = 0
l[i] = flag
return l
看代码可能不慎明了,其实这是计算相同的前后缀来的。
以字符串abcab为例,它的next数组为[0,0,0,1,2],
其前三个字符没有公共的前后缀,所以都是0,到了第四位,模式串有了一个公共的前后缀,就是a然后其字符长度1,所以next[3] = 1
到了第五位,其公共的前后缀变成了ab,所以next[4] = 2
详细列如下:
next值 | 模式串 | 前缀 | 后缀 | 公共部分 |
---|---|---|---|---|
2 | abcab | abca,abc,ab,a | bcab,cab,ab,a | ab |
1 | abca | abc,ab,a | bca,ca,a | a |
0 | abc | ab,a | bc,c | 无 |
后面的就不用说了,所有的公共前后缀均为无
而公共的前后缀可以用来计算下次的src_index的偏移距离,如果模式串在匹配到src_index位置时不匹配了,可以根据之前src_index 的下次偏移的位置应该就是src_index += target_index - l[target_index - 1]
增量为模式串已经匹配的长度与next数组的在该处值的差
其实仔细想想就可以理解该处的意义,因为l[target_index - 1]其实代表的是之前最后匹配处的公共匹配值,如果没有匹配,那么偏移的距离则正好应该为target_index
说的比较复杂,实际上就是有2个问题需要弄清楚
- 为何src_index的偏移需要增加l[target_index - 1]
- 为何target_index的偏移需要回到l[target_index - 1]
target_index的偏移
先说这个,为何要回到l[target_index - 1]的位置,其实就是因为前面所说道的前后缀的概念。
当前的位置不匹配了,但是前面的内容其实都是匹配的,而匹配的内容,刚好跟前面有重复的前缀,所以,target_index 需要回到之前前缀的后面的位置,也就是l[target_index - 1]的位置。
src_index的偏移
因为l[target_index - 1]的偏移位置已经变化了,所以src_index也必须进行变化,才能够让我们匹配的位置对应起来。
举例来说:
假设匹配串为abcaabcaabcab,模式串为abcab,第一次匹配,在src_index = 0,target_index = 4时出现了第一次不匹配,但是abcab的l[target_index - 1] = 1,所以src_index挪移的距离就应该为3,target_index也回到了l[target_index - 1]的位置
这样就保证了匹配总能够保证最少,可以不进行重复匹配