KMP 真的一看就懂一做就错三天全忘 好像青年痴呆
遂烂笔头之
文章目录
1. 模式匹配
这里考察的是模式匹配问题
也就是我给定一个字符串 s,和一个子串 t,我想要找到 s 的一个子串 = t,也就是(在 s 中找到 t 进行匹配的任务)
此时将 s 称为目标串,子串 t 称为模式串
以下主要考虑两种方法:
- 朴素模式匹配,也就是暴力法,BF 算法
- 大名鼎鼎 KMP
看完可以在这里练手:
2.Brute-Force 算法
也就是暴力 = BF 方法
2.1 思路
考虑 s 目标串从第 i 个位置开始和 模式串 t 可以匹配上,则我枚举这里的起点 i:
- 从第一个位置 s[0] 开始,检查 s[0] == t[0]? s[1] == t[1]? … 直到匹配结束或者出现不匹配为止
- 从第二个为止 s[1] 开始,和 t 从头匹配,也就是 s[1] == t[0] ? s[2] == t[1]? …
- 循环到匹配上为止;或者当前的位置 i 后面已经没 t 这么长的剩余了,则返回 -1 表示 s 中不存在 t
一般使用的时候用双指针,i 表示当前匹配到 s 的第 i 位;j 表示匹配到 t 的第 j 位;当 s[i] ≠ t[j] 的时候说明匹配失败,j 回到 0,i 回到上一轮匹配开头位置的下一个
2.2 代码示例
提供一个 python 代码,基于 28. 找出字符串中第一个匹配项的下标
class Solution:
def strStr(self, s: str, t: str) -> int:
# 这里的 s 是主串 = 目标串,t 为子串 = 模式串
for start in range(len(s)-len(t)+1):
# start 表示从 s 的 start 位置开始匹配 t
i,j = start,0 # 双指针,指示当前匹配到 s 和 t 的哪一个位置了
while s[i] == t[j]:
i += 1; j += 1
if j == len(t): return start # 匹配成功
return -1
这里的 s 的长度为 m,t 的长度为 n,最坏的情况我每一轮匹配都是到最后才发现匹配不上(则每一轮匹配复杂度为 O(n)),同时真的匹配上要到 s 的最末尾(进行 O(m) 次匹配)
则此时的算法复杂度 = O(mn)
3. KMP 算法
考虑传统暴力方法:也就是我这一轮匹配从 s 的 start 位置开始,如果匹配失败了,我就从 s 的 start + 1 位置开始尝试下一次;
所以说这里的复杂度会是 mn,最坏的情况我要把 (s 的每一个字符)都当作和 t 的一轮匹配的开头来尝试一次,会很麻烦
KMP 的思路即:我如果以 start 为开头,匹配到了 start + k 才发现 start + k + 1 匹配不上了,但是我这轮已知了 s 的 start → start + k 和 t 的前 k + 1 个字符是一样的,则我想要利用这个信息快速筛选掉一些我已知不可能的 s 的 start,也就是不再需要将 s 的每一个字符都作为可能的开头来尝试一次了
3.1 整体思路
假设此时我对下面的 s 和 t 进行匹配;一开始以 s 的第一个元素作为匹配的开头进行匹配:
匹匹匹匹到 a vs c 的时候发现不匹配了,如果是 BF 方法,则此时下一个 start 设置为 s 的第二个字符(也就是 b),t 回到开头,重新开始尝试匹配
但是本轮匹配的时候,我已经发现了 t 的前四个和 s 的前四个是一样的,则我考虑能不能跳过一部分的开头
假设我还是以 s 的第二个元素为新的 start,t 也回到开头,重新开始匹配
匹配顺序如图,但是注意 s 的绿色部分和 t 的绿色部分是相等的,s 的绿色的部分的信息我已知;则这里的第二轮匹配的前几个检查,其实也就是在检查下图的蓝色部分是否是一模一样的:
而这里的黑框的部分,也就是 t 的绿色部分的前三个元素 和 后三个元素,这一轮的匹配要是能成功,就要求 t 的绿色部分的 (前三个元素 = 后三个元素)
当然是不行的(。我不用看 s 只用看 t 就知道,这三个元素不相等,则以 s 的下标 1 的元素(=b) 作为 start 来进行匹配肯定不行,这个 start 尝试都不用尝试;
则此时尝试按照 BF ,再把 t 后移一位
同理,这里如果要匹配成功,则 t 的绿色部分的(前两个字符)和 t 的绿色部分的(后两个字符)需要一样;注意到这里还真的是一样的,说明以 s 的下标为 2 的字符(=a)作为 start 来匹配可能有戏
综上,我考虑:
- 我在第 x 轮匹配中,t 和 s 的 start 位置对齐,检查到 k 位置的时候发现 t 和 s 匹配不上,这轮匹配失败
- 但是我通过这轮匹配知道了 s 的( start → start + k-1) 和 t 的(0 → k-1)是一样的,这里的匹配上了的长度为 k 的区域就是上面的绿色部分
- 下一轮,如果我将 t 后移 x 个,则这轮匹配可行需要(t 的绿色部分的倒数 k-x 个 = 蓝色黑框部分)和(t 的绿色部分的开头 k-x 个 = 蓝色黑框部分)相同才行
综上,我从 start 开始对齐匹配失败了,则 start = start + 1 开始新的一轮匹配要成功,则需要绿色部分的倒数 k-1 个 = 开头的 k-1 个才行;同理,从 start = start + 1 开始新的一轮匹配要成功,则需要绿色部分的倒数 k-2 个 = 开头的 k-2 个才行 …
那我只要知道最大的一个 k0,使得(t 的前 k 个元素 = 绿色部分)的(倒数 k0 个)=(开头 k0 个) 就行了?因为这里的 k0 是最大的,则 k-1 → k-2 → … → k-k0+1 的这些检查一定不成功,也就是说 start + 1 / + 2 / + … / + k0-1 这些新的 start 都一定不会成功,只要跳过就行;
为了实现上面的判断,我对于任意的一个 k(任意长度的绿色部分)都要知道对应的最大的 k0,使得这个绿色部分的倒数 k0 个 = 开头 k0 个,我用一个数组 next[i] 来进行记录;同时注意这里的 k0 当然不能 = k,否则自己等于自己没有意义;即:
n e x t [ i ] = m a x { k 0 ∣ t [ i − k 0 : i ] = = t [ : k 0 ] , k 0 < k } next[i] = max \{k_0| t[i-k_0: i] == t[:k_0], k_0 < k\} next[i]=max{k0∣t[i−k0:i]==t[:k0],k0<k}
注意这里的绿色部分是不包括 t 的第 k 个元素的
综上,得到 next[i] 后,我的匹配算法可以精简为:
- t 和 s 的 start 位置对齐,进行匹配;到 t[k] 的时候发现匹配不成功
- start += k-next[k];
- 主串 s 的指针:i = max(start, i)
- 其实这里的 i 是可以不动的,只是要处理一下被 start 超过的情况
- 模式串 t 的指针:j = max(next[k], 0)
- 只是处理一下 k = 0,一个都没匹配上的情况,此时 next[0] = -1
- 继续尝试匹配
3.2 计算 next[i]
那么现在问题就是怎么得到 next[i]
注意到 next[i] 和 s 是无关的,它是针对 t 做的预处理
3.2.1 基本思路
再搬下来一遍 n e x t [ i ] = m a x { k 0 ∣ t [ i − k 0 + 1 : i + 1 ] = = t [ : k 0 + 1 ] , k 0 < k } next[i] = max \{k_0| t[i-k_0+1: i+1] == t[:k_0+1], k_0 < k\} next[i]=max{k0∣t[i−k0+1:i+1]==t[:k0+1],k0<k}
规定 next[0] = -1,k = 0 的时候绿色部分为 0啥也没有 ;同理 next[1] = 0,这里的绿色部分只有一个字符,如果和自己相等了就是全匹配了,自己等于自己没意义;设置为 0
考虑如果我已知了 next[i],想要知道 next[i+1]
则分成两种情况:
- 情况一:t[i] == t[next[i-1]] 也就是说相较于 i-1,此时新加入的元素刚好可以顺着往下匹配,则这里的 next[i] = next[i-1] + 1
比如这里的 next[3] 考察的是黄色部分,此时倒数的一个(a) 和开头的一个(a)是相等的,我已知 next[3] = 1,则 next[4]?
注意到对于 next[4],t[3] 加入黄色部分,而 t[3] 和上一个 next[3] 匹配后的下一个位置接着匹配(也就是深色的两个 b),则此时 next[4] = next[3] + 1 = 2
- 情况二:此时两个深色位置不相等,也就是 t[i] != t[next[i-1]]
直观理解就是这里的新的 next[i+1] 不能再顺着 next[i] 匹配的成果下去了,比如下图:
这里的蓝色部分是 next[9] 匹配好的,但是对于 next[10] 来说因为 a!= c 无法接下去了;
则我这里的目标找到哪里可以继续匹配我 t[9] 位置的这个 a,且匹配上的位置尽量靠后(则可以保证 k0 尽量长),那也就是 t[9] 这个 a 前面的 abab 尽量也匹配上,则此时问题转移为蓝色的部分的倒数 k1 个能否和 t 开头的 k1 个匹配上,注意到了嘛,这个 t0 就存在 next[4] = next[next[i]],也就是关注蓝色部分的匹配情况的那个 next 存储着这里我要的 k1
则我一开始是在 next[9] 的基础上继续算 next[10],发现匹配不上,则我下一个就在 next[4] 的基础上继续匹配,也就是下图:
这下匹配上了!则 next[10] 也就是 next[4] + 1
当然也会有很刁钻的情况,就是匹配不上,比如:
这个 z 哪里都没有啊,则我还是不断考虑蓝色部分的最大的 k1,也就是 next[next[4]] = next[2],此时变成 0
则此时变成 t[9] 和 开头 t[0] 匹配,还是不行,直接返回 0
3.2.2. 整理
综上,计算 next[i] 按照下述步骤:
- 初始化,next[0] = 0, next[1] = 1
- 对于 next[i+1]
- prek0 = next[i-1] 标记上面蓝色部分的末尾
- 先看 next[i] == t[prek0]
- 如果成立,则 next[i] = prek0 + 1
- 如果不成立,prek0 = next[prek0],继续检查
- 直到这里的 prek0 = 0,但是还是 next[i] != t[0],则 next[i] = 1
一个基础的 python 代码
nxt = [0]*(len(t)+1) # 这里多计算一位,在某些任务有用(比如 lc 214)
nxt[0], nxt[1] = -1, 0
i = 2
prek0 = nxt[1]
while i < len(t)+1:
if t[prek0] == t[i-1]: nxt[i] = prek0 + 1; prek0 += 1; i += 1
# 表示跟着 next[i-1] 匹配上了
elif prek0 <= 0: nxt[i] = 0; prek0 = 0; i+=1
# 表示 prek0 已经为 0 了但是还是匹配不上;则 nxt 为 0,下一个
else: prek0 = nxt[prek0]
# 表示当前的 i 还有救,不断缩短蓝色部分尝试匹配它
3.3 整体代码
综上,此时匹配的思路为:
- 先预处理得到 next
- 开始匹配,第一轮 start = 0,将 t 和 s 的 start 位置对齐尝试匹配
- 匹配成功,返回 start 就是匹配成功的起始位置
- 匹配到 t[k] 的时候失败,
- 则取出 next[k],这里的 start = start + k-next[k]
- 主串 s 的指针:i = max(start, i)
- 模式串 t 的指针:j = max(next[k], 0)
- 如果当前从 s 的 start 位置开始剩下的部分不足 len(t),匹配失败,此时 s 中不存在 t 子串
class Solution:
def strStr(self, s: str, t: str) -> int:
# 预处理 计算 next
nxt = [0] * (len(t) + 1)
nxt[0], nxt[1] = -1, 0
i = 2; prek0 = 0
while i < len(t)+1:
if t[i-1] == t[prek0]: nxt[i] = prek0 + 1; prek0 += 1; i += 1
elif prek0 == 0: nxt[i] = 0; i += 1
else: prek0 = nxt[prek0]
# 进行匹配
start = 0
i,j = 0,0
while start + len(t) <= len(s):
while s[i] == t[j]:
i+=1; j+=1
if j == len(t): return start # 匹配成功,返回起始位置
k = j
start += k - nxt[k]
i = max(i, start) # 注意处理一个都匹配不上,start 直接 + 1 超过了 i 的情况
j = max(nxt[k], 0) # 小心 k = 0 的情况
return -1 # 匹配失败
整理仓促,存在错误 / 不足欢迎指出!期待进一步讨论~
转载请注明出处。知识见解与想法理应自由共享交流,禁止任何商用行为!