面向问题
KMP算法是一种字符串匹配算法,面对问题如下:
给你两个字符串
haystack
和needle
,请你在haystack
字符串中找出needle
字符串出现的第一个位置
题目来源:力扣(LeetCode)
如这个经典的例子:
haystack
:a a b a a b a a f
needle
:a a b a a f
原本的暴力匹配法,即在haystack
中从起始位置循环,判断长度为needle
的部分是否满足需求的方法,复杂度高,存在冗余工作。
暴力法
:
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
if not needle:
return 0
elif len(needle) > len(haystack):
return -1
else:
length = len(needle)
notfind = False
for i in range(0, len(haystack)+1-length):
if haystack[i:length+i] == needle:
notfind = False
return i
else:
notfind = True
if notfind:
return -1
KMP算法
人力寻找的机制是,我们的寻找过程出现不匹配的时候,会记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
所以KMP原理就在于此,不匹配的时候不会从i+1
的位置继续逐个判断,而是跳转至一个合适的位置,再开始逐个判断。这样说是因为,在笔者看来,这样从结果出发,更容易理解。
那么既然不跳转至i+1
,而是跳转到一个“合适”的位置,就涉及到如何寻找合适位置的问题,这里将其设想为一个函数:getNext
假设getNext
函数输入是needle
及其长度,返回是一个数组,并且数组下标p对应的位置就是当前不匹配的p合适位置。那么我们就可以对暴力匹配法进行修改:
分为3部分:
- p不是初始位置并且不匹配,则根据当前p从nextp中索引,将p赋值为合适位置
- p匹配,则p+1,继续循环,看下一组的2个字母是否对应
- p已经到头了,即
needle
已经遍历结束,则返回对应的开始位置,也就是i-p所指
代码如下:
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
if not needle:
return 0
elif len(needle) > len(haystack):
return -1
else:
lenn = len(needle)
lenh = len(haystack)
# 根据needle可以求得一个数组nextp,用来寻找合适位置
nextp = getNext(lenn, needle)
p = -1
for i in range(lenh):
while p >= 0 and needle[p+1] != haystack[i]:
p = nextp[p]
if needle[p+1] == haystack[i]:
p += 1
if p == a - 1:
return i + 1 - a
# 全部遍历完依然无返回值,则返回-1
return -1
next数组
接下来就是重要的求next数组的过程了,首先要知道对于给定的一个数组,其前缀后缀的一定的,基于此,我们可以直接编写函数,返回数组明确了当前位置的前缀后缀情况。
可以分3步理解:
- 初始化,因为数组长度为1时,不存在前后缀,故nextp[0] = -1
- 注意到i是从1开始的,所以就是对比0位置和1位置开始的元素,若相等,则k+1,同时在每一轮必定会将一个k值送入nextp;这种情况下,下一次就对比k指针与i指针分别向后移动一位的值
- 如果上一次相等(k>-1),这一次不相等,则在nextp数组中寻找k对应的值
关于第三点,一定要理清思路:(但是以下文字不如一些帖子的动图更加直接)
- 如果之前k=0,则表示只有一个匹配,那么k就直接回到了-1,相当于从第一个与当前的i匹配
- 如果之前k>=1,则k转移到上一个相同的索引,继续比较上一个索引的值与当前i是否匹配
- 在不断索引自身的时候(即一直不匹配),则k的值一直减小,直到从头开始匹配
这样getNext函数就可以得到前后缀的关系了,由于haystack
与needle
是一起向前移动,所以不匹配就直接返回上一个位置就好。
代码如下:
def getNext(lenn, needle):
nextp = ['' for i in range(lenn)]
k = -1
nextp[0] = k
for i in range(1, lenn):
while (k > -1 and needle[k+1] != needle[i]):
k = nextp[k]
if needle[k+1] == needle[i]:
k += 1
nextp[i] = k
return nextp
完整代码KMP算法
:
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
if not needle:
return 0
elif len(needle) > len(haystack):
return -1
else:
lenn = len(needle)
lenh = len(haystack)
# 根据needle可以求得一个数组nextp,用来寻找合适位置
nextp = getNext(lenn, needle)
p = -1
for i in range(lenh):
while p >= 0 and needle[p+1] != haystack[i]:
p = nextp[p]
if needle[p+1] == haystack[i]:
p += 1
if p == a - 1:
return i + 1 - a
# 全部遍历完依然无返回值,则返回-1
return -1
# 求nextp数组
def getNext(lenn, needle):
nextp = ['' for i in range(lenn)]
k = -1
nextp[0] = k
for i in range(1, lenn):
while (k > -1 and needle[k+1] != needle[i]):
k = nextp[k]
if needle[k+1] == needle[i]:
k += 1
nextp[i] = k
return nextp
总结
个人感觉,对于代码的理解,重点在于2个地方:
- getNext函数可以对于一个给定的字符串求其nextp的数组,每一个位置即为当前p不匹配的情况下应该调到的合适位置
- 在i进行循环的时候,出现了不匹配的情况,就去nextp中寻找下一个p的值,即合适位置
对于nextp还不理解的话,可以自己找例子,依照代码的循环,算一下,光看总是不能够很好的理解的。
参考资料
本文大量参考了代码随想录的力扣题解,在此表示感谢。