KMP算法
本文用python实现具体内容
用LeetCode28.找出字符串中第一个匹配项的下标举例.
难点:
- 理解KMP算法的匹配原理
- 理解next数组的求解方式
BF算法(朴素算法)
首先从BF算法(暴力匹配)开始理解字符串匹配。
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
n = len(haystack)
m = len(needle)
# 因为主串中每次要对比的只有i到i+m这一段,所以为不越界,将i的遍历上限变为n-m
for i in range(0, n - m + 1):
# 当匹配上直接返回下标i
if haystack[i: i + m] == needle:
return i
# 反之return -1
return -1
时间复杂度为最差为O(n*m),最好为O(n+m),当情况差且字符串过长时就得寄。
所以KMP算法应运而生,将匹配字符串的时间复杂度降到O(n+m),用空间换时间,多开了个next数组。
最长公共前后缀
要想理解KMP算法,首先要理解最长公共前后缀的概念。
前缀与后缀
给定一个字符串s = “abcba”,它的前缀为a,ab,abc,abcb,后缀为a,ba,cba,bcba.
即前后缀为不包含最后或最前的字符的所有连续子字符串。
所以,最长公共前后缀为前缀后缀中相同且最长的子串,在上述例子中为a。
KMP算法的匹配原理
首先我们知道公共前后缀是相同的,所以我们能将前后缀相替换。当匹配不上当前字符时,找模式串在当前字符的前一个字符所代表的最长公共前后缀长度,通过该前后缀长度,能直接跳过前缀,从前缀的后一个字符开始重新匹配。相对于BF算法,减少了时间花费。
这里的匹配原理其实用动图或者视频更好理解,可以上b站上找相关视频。推荐一个
给出相关匹配代码(无next数组的求解)
class Solution:
def getNext(self, needle: str): -> list:
....
return ne
def strStr(self, haystack: str, needle: str) -> int:
n = len(haystack)
m = len(needle)
ne = self.getNext(needle)
# 匹配代码
j = 0
for i in range(n):
# 当j大于0的目的是防止连续回退导致越界
# 当匹配不上时,就将j回退到下标j-1对应的前缀的下一个字符开始重新匹配
# while循环的意义在下面求解nxt数组中有讲解
while j > 0 and haystack[i] != needle[j]:
j = ne[j-1]
# 匹配上时,将j移动一位
if haystack[i] == needle[j]:
j += 1
# 当needle完全匹配完,返回haystack中第一个匹配项的下标
if j == m:
return i - j + 1
return -1
理解了匹配原理后,下面就是对定义跳几步的next数组进行求解。
求解next数组
由上面原理可以知道,next数组就是求模式串中每个字符对应的最长公共前后缀,下面给出具体代码。
def getNext(self, needle: str): -> list:
j = 0
n = len(needle)
ne = [0] * 1010
for i in range(1, n):
# j和i分别代表前缀的末端和后缀的末端
# 当匹配不上时,则找前一个字符对应的公共前缀的下一个字符,继续匹配,如果还是匹配不上,则继续回退,循环就是这么来的,直到j为0,说明退到起点了。
# 对于这一处的循环,B站上有更详细的图解视频,链接(https://www.bilibili.com/video/BV16X4y137qw),该视频进度条14:13后。
while j > 0 and ne[i] != ne[j]:
j = ne[j - 1]
# 当匹配上时,说明已经出现公共前后缀,继续往后遍历,找出最长
# j其实也代表最长公共前后缀的长度
if ne[i] == ne[j]:
j += 1
# 因为初始化了ne数组中的值都为0,所以可以将ne[i] = j放在if needle[j] == needle[i]中
# 如果没没有初始化ne数组中的值都为0,则不能将ne[i] = j放在if needle[j] == needle[i]中,
# 因为此时除了相等时要赋值给ne[i],当j==0时也要赋值给ne[i]
ne[j] = i
return ne
其实next数组的求解也是模式串和自己匹配的过程,用双指针求出对应的最长公共前后缀。
总结
给出全部代码先
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
n = len(haystack)
m = len(needle)
ne = [0] * 1010
# getNext()
j = 0
for i in range(1, m):
while j > 0 and needle[j] != needle[i]:
j = ne[j - 1]
if needle[j] == needle[i]:
j += 1
ne[i] = j
# 匹配
j = 0
for i in range(n):
while j > 0 and haystack[i] != needle[j]:
j = ne[j-1]
if haystack[i] == needle[j]:
j += 1
if j == m:
return i - j + 1
return -1
理解匹配原理其实不难,本人觉得最难的就是next数组中对于j的回退,其他多看几遍也就会了。