KMP
作者:互联网
KMP算法
目录
问题
例如:在文本串S(长度为N)中查找模式串P(长度为M)出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
Brute Force
一种暴力做法就是从txt的首字符开始配对txt和pat,配对不成功就从txt的下一个字符开始再做一次
时间复杂度:O(MN)
空间复杂度:O(1)
KMP
简介
Knuth-Morris-Pratt 字符串查找算法,简称为 KMP算法,常用于在一个文本串 S 内查找一个模式串 P 的出现位置。这个算法由 Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法。
算法逻辑
假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置
- 当前字符匹配成功(即S[i] == P[j],图一),都令i++,j++,继续匹配下一个字符;
- 当前字符匹配失败(即S[i] != P[j],图二),则令 i不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j]位。
换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值(next 数组的求解会在下文详细阐述),即移动的实际位数为:j - next [j],且此值≥1。
BF匹配失败,就把模式串P右移一位,而KMP匹配失败后,模式串P会移动 j - next [j] 位,缩短了时间是空间换时间的做法。
什么是next数组
原理:
取出文本串 S 和模式串 P 匹配成功的部分,计算这部分前后缀的公共元素,实际就是用模式串 P 的前缀去匹配文本串 S 的后缀。
列举出模式串P匹配文本串S过程中可能出现的几种情况以及对应匹配好的部分
序号 | 匹配情况 | |||||||
---|---|---|---|---|---|---|---|---|
1 | 第一位失配 | |||||||
2 | 第二位失配 | a | ||||||
3 | 第三位失配 | a | b | |||||
4 | 第四位失配 | a | b | a | ||||
5 | 第五位失配 | a | b | a | a | |||
6 | 第六位失配 | a | b | a | a | b | ||
7 | 第七位失配 | a | b | a | a | b | c | |
8 | 第八位失配 | a | b | a | a | b | c | a |
定义:
前缀为除了最后一个字符以外,一个字符串的全部头部组合;
后缀为除了第一个字符以外,一个字符串的全部尾部组合。
以第6种失配情况为例枚举出前后缀。
a | b | a | a | b |
---|---|---|---|---|
前缀 | ||||
a | ||||
a | b | |||
a | b | a | ||
a | b | a | a | |
后缀 | ||||
b | ||||
a | b | |||
a | a | b | ||
b | a | a | b |
最长的前后缀的公共元素为ab,长度为2
对所有匹配情况下做相同的操作,记录每个最长前后缀的公共元素长度。
失配位置索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
失配字符 | a | b | a | a | b | c | a | c |
前后缀公共元素最大长度 | 0 | 0 | 0 | 1 | 1 | 2 | 0 | 1 |
我们将其称为next数组或者fail数组,记录着字符串匹配过程中,发生字符失配时可以从那个位置开始重新匹配。
怎么求next数组?
下面用图示来说明:
当前匹配位置索引为 I (I=J+1),已知next[J]=K,想求出next[I]
- 红色:P[J]字符失配时,前后缀的公共元素。长度为**next[J] **
- 蓝色:P[J]字符
- 橙色:P[K]字符
如果P[J] = P[K],则皆大欢喜, next[I]= next[J]+1
如果P[J] !=P[K],只能寻找更短的相同前后缀匹配,我们看下图
- 灰色:P[K]字符失配时,前后缀的公共元素。长度为**next[K] **
- 紫色:P[next[K]]字符
- 蓝色:P[J]字符
因为红色部分是已经匹配好的,两部分红色都可以是灰+红+灰
如果P[J] = P[next[K]],则皆大欢喜, next[I] = next[K]+1 = next[ next[J] ]+1
如果P[J] !=P[next[K]],继续寻找更短的相同前后缀匹配
结论:求解某个位置(I)next值是一个循环过程,不断检查 上一位置(J)与其(J)最长前缀的后一位(K)是否相等。如果相等next[I] = next[J] + 1,否则 K = next[K]。通过递归索引K = next[K],就能找到长度更短的相同前缀后缀
n = len(S)
m = len(P)
# ———— 构建next ————————————————————————————————————————————
Next =[0,0]
k = 0
for i in range(2,m):
j = i-1
k = Next[j]
# 匹配失败,递归k寻找更小的前缀,直到为0
while k and P[k] != P[j]:
k = Next[k]
# 匹配成功
if P[k] == P[j] :
k+=1
Next.append(k)
else:
Next.append(0)
next数组的优化
比如,如果用之前的next 数组方法求模式串“abab”的next 数组,可得其next 数组为0 0 0 1,当它跟下图中的文本串去匹配的时候,发现S[3]跟P[3]失配。
于是去匹配S[3]和P[1]。
还是失配,事实上,S[3]与P[3]失配,拿与P[3]相等的P[1]去继续匹配S[3],结果肯定是失配。
所以写出next数组的时候要检查一遍,如果P[I] =P[next[I]],递归next[I] = next[next[I]]。
所以,咱们得修改下求next 数组的代码。
# —————— 优化next——————————————————————————————————————
for i in range(2,m):
j = i-1
l=Next[i]
while l and P[i]==P[l]:
l=Next[l]
if P[i] != P[l]:
Next[i] = l
else:
Next[i] = 0
最终代码
class Solution:
def strStr(self, S: str, P: str) -> int:
n = len(S)
m = len(P)
# 特别地,模式串为空字符
if m == 0:
return 0
# ———— 构建next ————————————————————————————————————————————
Next =[0,0]
k = 0
for i in range(2,m):
j = i-1
k = Next[j]
# 匹配失败,递归k寻找更小的前缀,直到为0
while k and P[k] != P[j]:
k = Next[k]
# 匹配成功
if P[k] == P[j] :
k+=1
Next.append(k)
else:
Next.append(0)
# —————— 优化next——————————————————————————————————————
for i in range(2,m):
j = i-1
l=Next[i]
while l and P[i]==P[l]:
l=Next[l]
if P[i] != P[l]:
Next[i] = l
else:
Next[i] = 0
# —————— 开始匹配——————————————————————————————————————
# i:文本串匹配字符索引
# j:模式串匹配字符索引
j = 0
for i in range(n):
# 匹配不成功,递归j,直到j=0
while j and S[i] != P[j]:
j=Next[j]
if S[i] == P[j]:
j+=1
#退出匹配询问
if j == m:
return i-j+1
return -1
参考资料
https://zhuanlan.zhihu.com/p/76348091
https://www.jianshu.com/p/a9b270eb09bb
https://www.cnblogs.com/zhangtianq/p/5839909.html
https://zhuanlan.zhihu.com/p/76348091
标签:字符,匹配,后缀,next,KMP,Next,失配 来源: https://www.cnblogs.com/jiannan-blog/p/13136778.html