KMP算法简洁版及其python实现

面向问题

KMP算法是一种字符串匹配算法,面对问题如下:

给你两个字符串haystackneedle,请你在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部分:

  1. p不是初始位置并且不匹配,则根据当前p从nextp中索引,将p赋值为合适位置
  2. p匹配,则p+1,继续循环,看下一组的2个字母是否对应
  3. 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. 初始化,因为数组长度为1时,不存在前后缀,故nextp[0] = -1
  2. 注意到i是从1开始的,所以就是对比0位置和1位置开始的元素,若相等,则k+1,同时在每一轮必定会将一个k值送入nextp;这种情况下,下一次就对比k指针与i指针分别向后移动一位的值
  3. 如果上一次相等(k>-1),这一次不相等,则在nextp数组中寻找k对应的值

关于第三点,一定要理清思路:(但是以下文字不如一些帖子的动图更加直接)

  • 如果之前k=0,则表示只有一个匹配,那么k就直接回到了-1,相当于从第一个与当前的i匹配
  • 如果之前k>=1,则k转移到上一个相同的索引,继续比较上一个索引的值与当前i是否匹配
  • 在不断索引自身的时候(即一直不匹配),则k的值一直减小,直到从头开始匹配

这样getNext函数就可以得到前后缀的关系了,由于haystackneedle是一起向前移动,所以不匹配就直接返回上一个位置就好

代码如下:

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个地方:

  1. getNext函数可以对于一个给定的字符串求其nextp的数组,每一个位置即为当前p不匹配的情况下应该调到的合适位置
  2. 在i进行循环的时候,出现了不匹配的情况,就去nextp中寻找下一个p的值,即合适位置

对于nextp还不理解的话,可以自己找例子,依照代码的循环,算一下,光看总是不能够很好的理解的。

参考资料

本文大量参考了代码随想录的力扣题解,在此表示感谢。

猜你喜欢

转载自blog.csdn.net/qq_45510888/article/details/119517124
今日推荐