KMP算法PMT数组与next数组构造解释

从零开始,静心学习

 
 

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开始处理。
KMP算法PMT数组与next数组构造解释_第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都前进一步后,我们就会遇到下面的情况。
KMP算法PMT数组与next数组构造解释_第2张图片

此时i, j = 2, 1,我们匹配p[i]p[j]。如果相等,那么和前面的情况一样,当前位置的PMT值PMT[i] = PMT[i - 1] + 1,加一就行了。
但是当前的p[i] != p[j],这时候我们需要其他的操作。这里我用其他位置处的匹配情况来说。

步骤三

KMP算法PMT数组与next数组构造解释_第3张图片

此时后缀B == 前缀C,和第一幅图一样,这时都将i和j前进一步,但是前进一步,就会遇到第二幅图所出现的情况。
KMP算法PMT数组与next数组构造解释_第4张图片
此时后缀B != 前缀C,所以我们这是要将眼光放在该索引位置前面的前缀与后缀。我们再看下面一个图。
KMP算法PMT数组与next数组构造解释_第5张图片
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]

所以我们会得到下面的状态。
KMP算法PMT数组与next数组构造解释_第6张图片

这是我们比较主串的p[5]与子串的p[1]是否相同,然而很不幸,还是不相同。这个时候同样的,我们的索引j还要跳转,此时j = PMT[j - 1] = PMT[0] = 0,就会得到下图。
KMP算法PMT数组与next数组构造解释_第7张图片
这个时候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|1next=1,j=0max{k1<k<j,p[0]...p[k1]==p[jk]...p[j1]}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的。

你可能感兴趣的:(数据结构与算法,算法)