当前位置: 首页 > article >正文

数据结构:利用递推式计算next表

next 表是 KMP 算法的核心内容,下面介绍一种计算 next 表的方法:利用递推式计算

如图 6.3.1 所示,在某一趟匹配中,当对比到最后一个字符的时候,发现匹配失败(s[i] ≠ t[j])。根据 BF 算法,则令 i = i - j + 1; j=0 ,两个字符串的指针同步回退,然后从该位置开始继续比对。

在这里插入图片描述

图 6.3.1 一趟匹配失败

在下一趟匹配过程中,对于模式串 t 而言,还要从头开始,尽管有一些字符在前一趟匹配中已经比对过且成功。如此就造成了时间上的浪费。

以图 6.3.1 为例,s[i-j, i) 完全是由 0 组成的,那么,在 i, j 回退之后的下一趟比对中,前 j − 1 j-1 j1 次比对必然会成功。因此,可以令 i 不回退,保持不变,令 j = j - 1 。这样,就减少了比对次数。

上述“令 i 保持不变、j = j - 1”的含义,可以理解为:令 t 相对于 s 向右移动一个单元,然后从前一个匹配失败位置继续比对,如图 6.3.2 所示。

在这里插入图片描述

图 6.3.2 i 保持不变, j=j-1

将上述案例推广到稍微一般的情况,如图 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.3 t 向右移动 3 个字符

将上面的讨论,再向更一般化的情况推广,如图 6.3.4 所示,假设前一趟比对终止于 s[i] ≠ t[j] ,按照上述操作,指针 i 不必回退,而是将 s[i]t[k] 对齐,并进行下一趟比对。

那么,问题是 k 应该如何确定?

在这里插入图片描述

图 6.3.4 模式串的移动距离

结合图 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)={0k<j  t[0,k)=t[jk,j)}
一般地,该集合可能包含多个符合条件的 k 。但需要特别注意的是,其中具体由哪些 k 值构成,仅取决于模式串 t 以及前一趟比对的首个匹配失败位置 t[j] ,而与主串 s 无关。

从图 6.3.4 还可以看出,若下一轮比对从 s[i]t[k] 的比对开始,这等效于将 t 向右移动 j - k 个单元,位移量与 k 负相关。因此,为保证 ts 的对齐位置不倒退(指针 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) 0N(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 所示。

在这里插入图片描述

图 6.3.5 计算 next[j+1]

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 .

在这里插入图片描述

图 6.3.6 递推计算

既然总有 next[k] < k ,故在此过程中 k 必然严格递减;同时,即便 k 降低至 0,亦必然会终止于通配的next[0] = -1,而不致下溢。如此,该算法的正确性完全可以保证。

由此,就可以创建 next 表。

例 6.3.1 已知串 t = '000010' ,计算 next 表(或称:next 数组值)。

【解】

  1. next[0] = -1

  2. 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] = 0t[k] = t[-1] = * ,相等(通配),所以:

    next[j] = k+1 (因为 next[j-1] = k ,),

    next[1] = -1+1 = 0

  3. j = 2 时,k = next[j-1] = next[1] = 0

    因为 t[j-1=1] = 0t[k=0] = 0 ,相等,所以 next[j=2] = k + 1 = 0 + 1 = 1

  4. j = 3 时,k = next[2] = 1

    因为 t[j-1=2] = 0t[k=1] = 0 ,相等,所以 next[j=3] = k + 1 = 1 + 1 = 2

  5. j = 4 时,k = next[j-1=3] = 2

    因为 t[j-1=3] =0t[k=2] = 0 ,相等,所以 next[j=4] = k + 1 = 2 + 1 = 3

  6. j = 5 时,k = next[j-1=4] = 3

    因为 t[j-1=4]=1t[k=3] = 0 ,不相等。

    ​ 则 k = next[k=3] = 2 :因为 t[j-1=4]=1t[k=2] = 0 ,不相等。

    ​ 则 k = next[k=2] = 1 :因为 t[j-1=4]=1t[k=1] = 0 ,不相等。

    ​ 则 k = next[k=1] = 0 :因为 t[j-1=4]=1t[k=0] = 0 ,不相等。

    ​ 则 k = next[k=0] = -1 :因为 t[j-1=4]=1t[k=-1] = * ,相等(通配)。所以 next[j=5] = k + 1 = -1 + 1 = 0

Index-1012345
t[ ]*000010
next[]N/A-101230

http://www.kler.cn/a/610857.html

相关文章:

  • 《jQuery Mobile 页面:深入解析与优化实践》
  • 接口/UI自动化面试题
  • 力扣hot100_二分查找
  • 在线问卷调查|在线问卷调查系统|基于Spring Boot的在线问卷调查系统的设计与实现(源码+数据库+文档)
  • node-red dashboard
  • 在 ASP .NET Core 9.0 中使用 Scalar 创建漂亮的 API 文档
  • Android14 原生PackageInstaller安装某些apk报错问题
  • Qt的文件操作
  • MySQL无法链接
  • Oracle 数据库通过exp/imp工具迁移指定数据表
  • WordPress 代码高亮插件 io code highlight
  • 推荐一款好看的 vue3 后台模板
  • 【Unity3D实现UI轮播效果】
  • Spring Boot 自定义 Starter 组件的技术指南
  • 通过按键控制stm32最小系统板上LED的亮灭状态
  • 全文 - MLIR Toy Tutorial Chapter 1: Toy Language and AST
  • CAT1模块 EC800M HTTP 使用后续记录
  • PSA方法计算器(PSA Method Calculator): 鼠标完美灵敏度测试网站
  • 【计算机网络】计算机网络协议、接口与服务全面解析——结合生活化案例与图文详解
  • Jmeter:常用线程组设置策略