next 表是 KMP 算法的核心内容,下面介绍一种计算 next 表的方法:利用递推式计算
如图 6.3.1 所示,在某一趟匹配中,当对比到最后一个字符的时候,发现匹配失败(s[i] ≠ t[j]
)。根据 BF 算法,则令 i = i - j + 1; j=0
,两个字符串的指针同步回退,然后从该位置开始继续比对。
在下一趟匹配过程中,对于模式串 t
而言,还要从头开始,尽管有一些字符在前一趟匹配中已经比对过且成功。如此就造成了时间上的浪费。
以图 6.3.1 为例,s[i-j, i)
完全是由 0
组成的,那么,在 i, j
回退之后的下一趟比对中,前
j
−
1
j-1
j−1 次比对必然会成功。因此,可以令 i
不回退,保持不变,令 j = j - 1
。这样,就减少了比对次数。
上述“令 i
保持不变、j = j - 1
”的含义,可以理解为:令 t
相对于 s
向右移动一个单元,然后从前一个匹配失败位置继续比对,如图 6.3.2 所示。
将上述案例推广到稍微一般的情况,如图 6.3.3 所示,在某一轮匹配中,s[i]='E' ≠ 'O'=t[4]
,在此为止匹配失败后,由于 s[i-4, i) = t[0, 4) = 'REGR'
,即 s[i]
左侧的若干字符已经匹配,所以,接下来只需将 t[0]
与 s[i-1]
对齐即可,或者说将 t
向右移动 4-1=3
个字符,等效于 i
保持不变,同时令 j = 1
,然后继续匹配。
将上面的讨论,再向更一般化的情况推广,如图 6.3.4 所示,假设前一趟比对终止于 s[i] ≠ t[j]
,按照上述操作,指针 i
不必回退,而是将 s[i]
与 t[k]
对齐,并进行下一趟比对。
那么,问题是 k
应该如何确定?
结合图 6.3.4 以及前面两个案例,经过前一趟比对,已经确定匹配的范围是:t[0, j) = s[i-j, i)
。
于是,若模式串 t
经适当向右移动之后,能够与 s
的某一子串完全匹配(包含 s[i]
在内),则其中的必要条件是:t[0, k) = s[i-k, i) = t[j-k, j)
。
也就是,在 t[0, j)
中长度为 k
的真前缀 t[0, k)
,应与长度为 k
的真后缀 t[j-k, j]
完全匹配。
空串是任何字符串的子串,也是任何字符串的前缀和后缀; 任何字符串都是自己的子串,也是自己的前缀和后缀。此类子串、前缀和后缀分别称作平凡子串(trivial substring)、平凡前缀(trivial prefix)和平凡后缀(trivial suffix)。
字符串本身之外的所有非空子串、前缀和后缀,分别称作真子串(proper substring)、 真前缀(proper prefix)和真后缀(proper suffix)。
所以,k
必然来自集合:
N
(
t
,
j
)
=
{
0
≤
k
<
j
∣
t
[
0
,
k
)
=
t
[
j
−
k
,
j
)
}
N(t, j) = \{0\le k\lt j~|~t[0, k) = t[j-k,j)\}
N(t,j)={0≤k<j ∣ t[0,k)=t[j−k,j)}
一般地,该集合可能包含多个符合条件的 k
。但需要特别注意的是,其中具体由哪些 k
值构成,仅取决于模式串 t
以及前一趟比对的首个匹配失败位置 t[j]
,而与主串 s
无关。
从图 6.3.4 还可以看出,若下一轮比对从 s[i]
与 t[k]
的比对开始,这等效于将 t
向右移动 j - k
个单元,位移量与 k
负相关。因此,为保证 t
与 s
的对齐位置不倒退(指针 i
不回退),同时又不至于遗漏任何可能得匹配,应在集合
N
(
t
,
j
)
N(t, j)
N(t,j) 中挑选最大的 k
。也就是说,当有多个 k
值时,即多个向右移动 t
的方案时,应该保守地选择其中移动距离最短者,即取 k
最大。于是,令:
next[j]
=
max
(
N
(
t
,
j
)
)
\text{next[j]} = \max(N(t,j))
next[j]=max(N(t,j))
则一旦发现 t[j]
与 s[i]
匹配失败,即可转而将 t[ next[j] ]
与 s[i]
对齐,并从这一位置开始继续下一趟比对。
既然集合
N
(
t
,
j
)
N(t, j)
N(t,j) 仅取决于模式串 t
以及匹配失败位置 j
,而与主串 s
无关,作为其中的最大元素, next[j]
也必然具有这一性质。于是,对于任一模式串 t
,不妨通过预处理提前计算出所有位置 j
所对应的next[j]
值,并整理为表格(即下面介绍的“next 表”),以便此后反复查询——亦即,将“记忆力”转化为“预知力”。
根据 N ( t , j ) N(t,j) N(t,j) 集合可知,只要 j > 0 j\gt 0 j>0 ,必然有 0 ∈ N ( t , j ) 0\in N(t,j) 0∈N(t,j) 。此时集合 N ( t , j ) N(t,j) N(t,j) 非空,从而可以保证“在其中能够取到最大值”,即 next[j] = max ( N ( t , j ) ) \text{next[j]} = \max(N(t,j)) next[j]=max(N(t,j)) 。
- 令
next[0] = -1
当
j
=
0
j=0
j=0 时,即 t
中的第一个字符与 s
中的字符不匹配,集合
N
(
t
,
j
)
N(t,j)
N(t,j) 为空集,此时,next[j=0]
的值应该是多少?
当某一趟匹配中,如果模式串 t
的第一个字符即匹配失败,根据匹配过程可知,应该将模式串 t
向右移动一个字符,然后启动下一趟匹配。为此,如果假想模式串 t
的首字符 t[0]
的左侧再有一个字符,可以记作 t[-1]
,并且该字符与任何字符都是匹配的。那么,在这趟本来是 t
的首字符匹配失败的操作中,就可以认为 t[-1]
与主串中对应字符匹配成功,于是“将模式串 t
向右移动一个字符”就可以认为是将 t[-1]
与 s[i]
对齐。
经过上述分析,当
j
=
0
j=0
j=0 时,可认为 next[0] = -1
。
- 计算
next[j + 1]
前面已经讲过,next[j]
与主串 s
无关,取决于模式串 t
以及匹配失败的位置 j
。于是,“对于任一模式串 t
,不妨通过预处理提前计算出所有位置 j
所对应的next[j]
值,并整理为表格(即下面介绍的“next 表”),以便此后反复查询”。
在确定了 next[0] = -1
之后,后续的 next[j]
可以使用递推方式计算。
假设已知 next[0]
到 next[j]
的值,由此递推出 next[j+1]
的值。
若 next[j] = k
,则意味着在 t[0, j)
中,自匹配的真前缀和真后缀的最大长度为 k
,故必然有:
next[j+1] ≤ next[j] + 1
而且,当且仅当 t[j] = t[k]
时取等号,即 next[j+1] = next[j] + 1 = k + 1
,如图 6.3.5 所示。
当 t[j] ≠ t[k]
时,则可以不断向前查找,next[next[j]] + 1, next[next[next[j]]] + 1, ...
,直到 t[-1]
这个与任何字符都匹配的字符,在这个过程中,必然会有能够满足 next[j+1] = next[... next[j] ...] + 1
成立的 t[k]
(如图 6.3.6 所示)。即:反复用 next[k]
替换 k
(令 k = next[k]
),一旦发现 t[j]
与 t[k]
匹配(含与 t[k = -1]
的通配),即可令 next[j + 1] = next[k] + 1
.
既然总有 next[k] < k
,故在此过程中 k
必然严格递减;同时,即便 k
降低至 0,亦必然会终止于通配的next[0] = -1
,而不致下溢。如此,该算法的正确性完全可以保证。
由此,就可以创建 next 表。
例 6.3.1 已知串 t = '000010'
,计算 next 表(或称:next 数组值)。
【解】
-
设
next[0] = -1
。 -
j = 1
时,k = next[j-1] = next[0] = -1
(此时,前面的 next 数组值已经得到,所以必然有next[j-1] = k
,由此可以计算得到k
)。比较
t[j-1]
和t[k]
(因为next[j] ≤ next[j-1] + 1
,当且仅当t[j-1] = t[k]
时取等号):t[j-1] = t[0] = 0
,t[k] = t[-1] = *
,相等(通配),所以:next[j] = k+1
(因为next[j-1] = k
,),next[1] = -1+1 = 0
。 -
j = 2
时,k = next[j-1] = next[1] = 0
。因为
t[j-1=1] = 0
,t[k=0] = 0
,相等,所以next[j=2] = k + 1 = 0 + 1 = 1
。 -
j = 3
时,k = next[2] = 1
。因为
t[j-1=2] = 0
,t[k=1] = 0
,相等,所以next[j=3] = k + 1 = 1 + 1 = 2
。 -
j = 4
时,k = next[j-1=3] = 2
。因为
t[j-1=3] =0
,t[k=2] = 0
,相等,所以next[j=4] = k + 1 = 2 + 1 = 3
。 -
j = 5
时,k = next[j-1=4] = 3
。因为
t[j-1=4]=1
,t[k=3] = 0
,不相等。 则
k = next[k=3] = 2
:因为t[j-1=4]=1
,t[k=2] = 0
,不相等。 则
k = next[k=2] = 1
:因为t[j-1=4]=1
,t[k=1] = 0
,不相等。 则
k = next[k=1] = 0
:因为t[j-1=4]=1
,t[k=0] = 0
,不相等。 则
k = next[k=0] = -1
:因为t[j-1=4]=1
,t[k=-1] = *
,相等(通配)。所以next[j=5] = k + 1 = -1 + 1 = 0
。
Index | -1 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|---|
t[ ] | * | 0 | 0 | 0 | 0 | 1 | 0 |
next[] | N/A | -1 | 0 | 1 | 2 | 3 | 0 |