从零开始,静心学习
1. 前言
KMP算法是用于搜索子串的经典算法,其中重点就在于利用了next数组减少了很多重复的搜索,这里不细讲KMP算法是怎么进行搜索的,我尽可能地将next的数组构造中的一些当时令我困惑的问题讲解清楚。
2. PMT(Partial Match Table)数组定义
首先,我觉得这next数组构造一部分很模糊的主要原因是很多定义大家都不讲清楚,这里我只讲自己的理解方法。这里我先不讲next数组。
首先,KMP算法要用到最长公共前后缀的长度。
解释一
最长公共前后缀:首先,这里我们都把前缀和后缀都认为是真前缀和真后缀。
举个例子:有一个字符串"aabaaf",它的前缀有{“a”, “aa”, “aab”, “aaba”, “aaaa”},我们不认为字符串本身"aabaaf"是它的前缀。同理它的后缀有{“abaaf”, “baaf”, “aaf”, “af”, “f”},我们不认为字符串本身"aabaaf"是它的后缀。可以看到字符串"aabaaf"并没有公共的前后缀,所以最长的公共前后缀的长度为0。
不仅如此,对于字符串"aabaaf",我们还需要知道它的所有从头开始的子串(这里包括{“a”, “aa”, “aab”, “aaba”, “aabaa”, “aabaaf”})的最长公共前后缀的长度,这个时候我们就需要用到PMT数组。
解释二
下面的表就是该字符串的PMT数组。这里我强调非常非常重要的一点,即PMT[i]表示的是直到索引为i的子串p[: i + 1]的最长公共前后缀的长度
。举个例子,比如PMT[4] = 2
表示的是"aabaa"的最长公共前后缀的长度,而不是"aaba"的最长公共前后缀的长度。“aabaa"的前缀有{“a”, “aa”, “aab”, “aaba”},后缀有{“abaa”, “baa”, “aa”, “a”},公共前后缀有{“a”, “aa”},最长的公共前后缀为"aa”,所以PMT[4]的值就是"aa"的长度,即PMT[4] = 2
。
char | a | a | b | a | a | f |
---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 |
value | 0 | 1 | 0 | 1 | 2 | 0 |
3. PMT数组的求解
接下来就是怎么求解一个字符串的PMT数组。
步骤一
首先对所有的非空字符串都有PMT[0] = 0
,所以我们从下标i = 1
开始处理。
解释三
这里的主串和子串都是模式串,其中找最长公共前后缀的方式与文本串与模式串的匹配类似。
为了找到当前位置的最长公共前后缀,我们可以认为主串是在找相同的后缀,子串只在找相同的前缀。在上面的图中,B就是前缀,C就是后缀。又因为主串和子串完全相同,所以A == B,又由于我们在匹配过程中一直B == C,所以一定有A == B。所以看似是两个字符串之间的比对,但实际上还是找同一个字符串,也就是模式串的最长公共前后缀。
上图中i, j = 1, 0
时有p[i] == p[j]
,此时我们很自然地能够得到一条规律,PMT[i] = PMT[i - 1] + 1
并且有i += 1, j += 1
步骤二
当i与j都前进一步后,我们就会遇到下面的情况。
此时i, j = 2, 1
,我们匹配p[i]
与p[j]
。如果相等,那么和前面的情况一样,当前位置的PMT值PMT[i] = PMT[i - 1] + 1
,加一就行了。
但是当前的p[i] != p[j]
,这时候我们需要其他的操作。这里我用其他位置处的匹配情况来说。
步骤三
此时后缀B == 前缀C,和第一幅图一样,这时都将i和j前进一步,但是前进一步,就会遇到第二幅图所出现的情况。
此时后缀B != 前缀C
,所以我们这是要将眼光放在该索引位置前面的前缀与后缀。我们再看下面一个图。
当p[5] != p[2]
时我们该怎么办呢?我们要想到我们是要找当前位置的最长公共前后缀,所以我们要关注当前位置前面的PMT值,也就是PMT[j - 1]
。我们尝试在旧后缀与旧前缀里面找到他们的最长公共前后缀。因为B == C,又因为E是B的最长公共后缀,F是C的最长公共前缀,所以我们一定有E == F。所以我们实际上也就是要比较新的后缀E后面的一个字符"f"与新的前缀F后面的一个字符"a"是否一样。我们知道"f"就是当前p[i]的取值,而怎么得到前缀F后面一个字符呢,实际上PMT[j - 1]就是这里的F后面的"a"的索引。所以这就是索引j的跳转规律,即j = PMT[j - 1]
。
所以我们会得到下面的状态。
这是我们比较主串的p[5]与子串的p[1]是否相同,然而很不幸,还是不相同。这个时候同样的,我们的索引j还要跳转,此时j = PMT[j - 1] = PMT[0] = 0
,就会得到下图。
这个时候p[5] != p[0]
,这个时候我们按照之前的规律j还是要跳转的,但是此时如果跳转j = PMT[j - 1]
,而此时PMT[j - 1] = PMT[-1]
,也即是j就要跳转到PMT[-1]的值。在python里这个指的是最后索引位置处的值,然而这是不合理的。所以我们还要加一条规则,当j = 0
时,如果p[i] == p[j]
,那么可以继续往后匹配;如果p[i] != p[j]
,那么这个时候就表明当前索引位置上的PMT值PMT[i] = 0
。
还有最后一个问题,在上面的规则中,如果在代码中写PMT[i] = PMT[i - 1] + 1
,这样得到的实际上并不是正确的结果。因为这样的前提是你的索引j没有跳转过,才会有这个规则。那么怎么改变呢?在j没有跳转过的情况下有j = PMT[i - 1]
,而j跳转过的情况下有PMT[i] = j + 1
,所以最终的代码如下。
# 原始版
def get_pmt(p):
n = len(p)
my_pmt = [0] * n
i, j = 1, 0
while i < n:
if p[i] == p[j]:
my_pmt[i] = j + 1
i += 1
j += 1
else:
if j == 0:
i += 1
else:
j = my_pmt[j - 1]
return my_pmt
# 简化版
def get_pmt(p):
n = len(p)
my_pmt = [0] * n
i, j = 1, 0
while i < n:
while j > 0 and p[i] != p[j]:
j = my_pmt[j - 1]
if p[i] == p[j]:
j += 1
my_pmt[i] = j
i += 1
return my_pmt
4. next数组的定义
首先声明,实际上我觉得next数组没什么用。在实际用kmp算法时,能够得到上面的PMT我觉得就足够了,没必要再去纠结减一还是不减一,右移还是不右移。
第二,关于next数组的定义,本文采用如下形式:
n e x t = { − 1 , j = 0 m a x { k ∣ 1 < k < j , p [ 0 ] . . . p [ k − 1 ] = = p [ j − k ] . . . p [ j − 1 ] } 0 , o t h e r next = \left \{ \begin{matrix} -1, j = 0 \\ max\{k|1<k<j, p[0]...p[k - 1] == p[j - k]...p[j - 1]\} \\ 0, other \end{matrix} \right. next=⎩⎨⎧−1,j=0max{k∣1<k<j,p[0]...p[k−1]==p[j−k]...p[j−1]}0,other
这里的next数组相比较上面的PMT数组一定是右移了一位的,即PMT[i] = next[i + 1]
,然后设置next[0] = -1
,但是后续的next数组里面的值是否还要减一这个是不一定的,这个在写代码的时候可以灵活调整。有的文章说设置next[0] = 0
会陷入死循环,但实际上在写代码时只要加上个if j == 0
的判断同样是可以避免的,但是我们按照定义来还是将next[0]
设为-1。最终我的结论是PMT数组没必要右移一位形成next数组,next数组也没必要在自己身上再减去一(注:这里不认同PMT数组与next数组是同一个对象,同时也不认同PMT数组在不右移的情况下自身减一然后形成next数组)。
4. next数组的求解
这里贴两个自己写的求next数组的代码,分别是PMT数组右移后不减一形成的next数组和PMT数组右移后再减一形成的next数组。
# 开头设置next[0] = -1,但是后续数组并不减一(即右移不减一)
def get_next(p):
n = len(p)
my_next = [0] * n
my_next[0] = -1
i, j = 1, 0
while i < n - 1:
while j > 0 and p[i] != p[j]:
j = my_next[j]
if p[i] == p[j]:
j += 1
my_next[i + 1] = j
i += 1
return my_next
# 开头设置next[0] = -1,同时后续数组减一(即右移再减一)
def get_next(p):
n = len(p)
my_next = [0] * n
my_next[0] = -1
i, j = 1, 0
while i < n - 1:
while j > 0 and p[i] != p[j]:
j = my_next[j]
if p[i] == p[j]:
j += 1
my_next[i + 1] = j - 1 # 实际上就是这里把j换成j - 1
i += 1
return my_next
最后再贴两段感想
注意我们确实求出了字符串的每个位置上的pmt的值。但是在实际利用pmt数组进行搜索时,我们是用不到pmt数组的最后一位的,即用不到pmt[n - 1]。因为假设模式串p的长度为n,并且在最后一位之前都匹配成功。这个时候我们指针i, j都右移一位,此时j = n - 1,即要匹配模式串都最后一个字符。此时很显然只有两种情况。如果匹配成功,则直接返回找到了该模式串,并且返回索引值;而如果没匹配成功,我们要调用的是pmt[j - 1] = pmt[n - 2]。所以我们是无论如何也使用不到pmt数组的最后一位的。但是注意,我们是有可能用到pmt[0]的。
首先我们要明确next数组一定是pmt数组右移来的。假设字符串p的长度为n,则next[i] = pmt[i - 1], i = 1, …, n - 2。所以pmt数组能用到pmt[0],但用不到pmt[n - 1];而next数组能用到next[n - 1],却用不到next[0]。但是next[0]是否一定要取0是不一定的。注意,这里即使tmp数组右移成了next数组后,next[0]也是可以取0的。