DC3算法:线性时间构建后缀数组的核心原理与工程实践
2026/8/26 7:27:22 网站建设 项目流程

1. 从字符串匹配到后缀数组:为什么我们需要DC3算法?

在文本处理、生物信息学乃至搜索引擎的底层,有一个看似简单却至关重要的问题:如何在一个庞大的文本中,快速找到所有出现某个特定模式串的位置?最朴素的想法是逐字符比较,但面对动辄GB、TB级别的数据,这种方法的效率是灾难性的。这就引出了字符串匹配领域的核心数据结构——后缀数组。后缀数组能让我们在对文本进行一次预处理后,以近乎“秒级”的速度回答后续大量的模式查询。然而,构建后缀数组本身,如果方法不当,也可能成为一个性能瓶颈。

后缀数组,简而言之,就是将给定字符串的所有后缀按照字典序排序后,记录其起始下标得到的数组。例如,对于字符串"banana",它的所有后缀是:"banana","anana","nana","ana","na","a"。按字典序排序后是:"a","ana","anana","banana","na","nana"。对应的起始下标数组[5, 3, 1, 0, 4, 2]就是后缀数组。

构建它的最直接方法是,生成所有后缀,然后用一个通用的排序算法(如快速排序)进行排序。比较两个后缀的字典序,最坏情况下需要比较整个字符串的长度,因此总时间复杂度是 O(N² log N),其中 N 是字符串长度。这显然无法处理大规模数据。于是,人们发明了倍增算法,将复杂度优化到了 O(N log N)。倍增算法的思想很巧妙:它并不直接比较完整的后缀,而是先比较每个后缀的前 1 个字符,排序得到初步排名;然后利用这个排名,比较每个后缀的前 2 个字符(可以看作由两个 1-字符的排名构成的新“元组”);接着比较前 4 个字符,以此类推。每次“倍增”,比较的单元长度翻倍,直到所有后缀的排名都不同为止。由于每次排序是基于上一轮的排名(可以离散化为整数),可以使用基数排序将单轮排序复杂度降至 O(N),总共进行 O(log N) 轮,因此总复杂度为 O(N log N)。

O(N log N) 对于许多应用已经足够好,但它仍然不是一个线性时间的算法。当 N 达到数亿甚至数十亿级别时(比如处理人类基因组或整个网页索引),log N 的因子也会带来显著的额外开销。更重要的是,从理论美的角度,我们能否达到最优的 O(N) 时间复杂度?这就是 DC3 算法登场的原因。DC3,全称 Difference Cover modulo 3,是一种精巧的、能够在最坏情况下以 O(N) 时间复杂度构建后缀数组的算法。它由 Kärkkäinen 和 Sanders 在 2003 年提出,其核心思想是“分治”与“递归”,通过精心挑选一部分后缀先进行排序,再利用这些已排序的后缀的信息,高效地推导出所有后缀的顺序。理解 DC3 不仅是为了应对极端的数据规模,更是深入理解字符串算法设计与分治思想的绝佳范例。它告诉我们,通过巧妙的问题转化和递归结构,可以突破直觉上的复杂度下限。

2. DC3算法的核心骨架:三分法与递归归约

DC3算法的精妙之处在于它规避了倍增算法中必须进行的多轮排序。它的目标是“一击即中”,通过一次递归调用就解决规模缩小的子问题,然后利用子问题的解一次性推导出原问题的解。整个算法的骨架可以分为三个清晰的阶段:采样、递归求解和归并。我们以一个具体的字符串S = "yabbadabbado"为例,其末尾我们添加一个字典序最小的哨兵字符$,得到S = "yabbadabbado$",长度 N=13。注意,哨兵$保证了每个后缀都是唯一的,并且它是全局最小的字符。

2.1 采样:模3分组与构造新字符串

DC3算法的第一步是采样。它并不是随机采样,而是按照下标模3的余数进行系统性地采样。我们将所有后缀的起始下标分为三类:

  • B0: 下标 i 满足 i mod 3 == 0 的后缀集合。
  • B1: 下标 i 满足 i mod 3 == 1 的后缀集合。
  • B2: 下标 i 满足 i mod 3 == 2 的后缀集合。

以我们的字符串为例,下标从0开始:

  • B0: 0, 3, 6, 9, 12 (后缀:"yabbadabbado$","badabbado$","abbado$","do$","$")
  • B1: 1, 4, 7, 10 (后缀:"abbadabbado$","adabbado$","bbado$","o$")
  • B2: 2, 5, 8, 11 (后缀:"bbadabbado$","dabbado$","ado$","$"? 注意,下标11是字符'o',后缀"o$",但下标11 mod 3 == 2,所以属于B2。这里下标12是$,属于B0)

算法的关键观察是:如果我们知道了 B1 ∪ B2 中所有后缀的排名,那么我们就可以在线性时间内推导出 B0 中所有后缀的排名,并最终完成所有后缀的归并排序。为什么是 B1 和 B2?因为它们的下标模3余1或2,这使得它们在比较时能够“对齐”,具体原因我们会在比较规则中看到。

因此,DC3算法递归求解的对象就是 B1 和 B2 的并集。为了递归,我们需要为这些后缀创建一个新的问题。具体做法是构造两个新的字符串:

  1. 对于每个起始于 B1 的后缀(i mod 3 == 1),我们取它开头的三个字符[S[i], S[i+1], S[i+2]]作为一个“元组”。如果i+2超出字符串边界(即接近末尾),我们用哨兵字符(如0)填充。
  2. 对于每个起始于 B2 的后缀(i mod 3 == 2),同样取三元组[S[i], S[i+1], S[i+2]]
  3. 将这些三元组按照它们在原字符串中出现的顺序拼接起来,形成一个新的整数数组R(Radix Array)。然后,我们对R这个数组进行离散化(即给每个不同的三元组分配一个唯一的整数排名),得到一个新的整数串R'

这个过程相当于把原字符串中所有起始位置模3不等于0的后缀的前三个字符“编码”成了一个新的字符串。R'的长度大约是原字符串的 2/3。我们对这个新的字符串R'递归地调用后缀数组构建算法。如果R'中所有字符(即排名)都不同,那么我们就直接得到了 B1∪B2 后缀的排序;如果有相同,递归调用会继续处理它,因为R'本身可以看作一个由整数构成的字符串。

2.2 递归求解:子问题的诞生与解决

现在我们有了一个更短的、由整数构成的新字符串R'。我们对R'递归地构建后缀数组。注意,R'的每个“字符”对应原字符串中一个起始于 B1 或 B2 的后缀的前三个字符的编码。因此,R'的后缀数组SA_R,其每个元素指向R'的一个后缀起始位置,这个位置映射回原字符串,就对应了一个 B1 或 B2 的后缀起始下标。

递归基是当新字符串R'的长度很小,或者其所有“字符”已经各不相同时,我们可以直接用基数排序等简单方法解决。递归调用返回的是R'的后缀数组,但我们真正需要的是原字符串中 B1 和 B2 后缀的排序顺序。根据SA_R和映射关系,我们可以直接得到这个顺序。假设我们得到了一个排名数组rank,其中rank[i]表示以位置i开头的后缀(i 属于 B1∪B2)在所有 B1∪B2 后缀中的名次。

2.3 归并:利用已知排名排序B0并完成最终合并

这是DC3算法最巧妙也最具技巧性的一步。现在我们手上有:

  1. B1 和 B2 中所有后缀的完整排名rank
  2. 尚未排序的 B0 中的后缀。

我们的任务是将 B0 中的后缀与已经排好序的 B1∪B2 后缀进行归并,得到最终完整的后缀数组。归并的关键在于:我们能否在线性时间内,比较一个 B0 后缀和一个 B1(或 B2)后缀的字典序?

答案是肯定的,并且我们可以利用已有的rank数组在 O(1) 时间内完成比较。比较规则基于一个简单的观察:任何一个后缀都可以由其第一个字符和它“后面”的那个后缀唯一确定。

  • 对于一个 B0 位置 i (i mod 3 == 0),它的后缀Suffix(i)。第一个字符是S[i]。如果S[i]不同,那么比较结束。如果S[i]相同,我们需要比较Suffix(i+1)。注意,i+1mod 3 == 1,属于 B1。而Suffix(i+1)的排名我们已经从rank数组中知道了!所以,比较Suffix(i)Suffix(j)(j 属于 B1∪B2)可以转化为:(S[i], rank[i+1])(S[j], rank[j+1])的元组比较。这是一个二元组比较,是 O(1) 的。
  • 类似地,如果需要比较两个 B0 后缀Suffix(i)Suffix(j),我们可以将其转化为三元组比较:(S[i], S[i+1], rank[i+2])(S[j], S[j+1], rank[j+2])。因为i+2mod 3 == 2,属于 B2,其排名也在rank中。

有了这个 O(1) 的比较器,我们就可以:

  1. 排序 B0 后缀:使用这个比较器,对 B0 中的所有后缀进行排序。因为比较是 O(1) 的,使用基于比较的排序算法(如快速排序)复杂度为 O(|B0| log |B0|),其中 |B0| ≈ N/3。但我们可以做得更好:注意到比较的键值(S[i], rank[i+1])是整数对,我们可以使用基数排序在 O(|B0|) 时间内完成对 B0 的排序。
  2. 归并所有后缀:现在我们有两条已经排好序的“链表”:一条是 B0 后缀列表,另一条是 B1∪B2 后缀列表。使用标准的双指针归并算法,在每一步用我们定义的 O(1) 比较器决定哪个后缀更小,将其放入最终的后缀数组。这个过程也是 O(N) 的。

至此,我们得到了完整字符串 S 的后缀数组。整个算法的时间复杂度递推式为 T(N) = T(2N/3) + O(N)。根据主定理,其解为 T(N) = O(N)。我们成功地在最坏情况下实现了线性时间的后缀数组构建。

3. 从理论到实现:DC3算法的关键细节与陷阱

理解了算法框架,实现起来还有不少魔鬼细节。这些细节处理不好,轻则效率低下,重则得到错误结果。

3.1 哨兵字符的处理与下标映射

哨兵字符$(或其他小于所有实际字符的符号)是必须的。它的作用有两个:一是保证每个后缀都是唯一的,避免排序时出现相等情况导致算法逻辑复杂化;二是在构造新字符串R时,用于填充不足三元的组。例如,一个起始于倒数第二个字符的 B1 后缀,可能只有两个字符(最后一个字符和哨兵)。我们需要用额外的哨兵(通常用0表示)将其填充为三元组。

下标映射是另一个容易出错的地方。原字符串下标i、新字符串R的下标k、以及递归后SA_R中的位置,这三者之间的映射关系必须清晰且一致。通常,我们会创建两个数组:

  • SA12:存储所有 B1 和 B2 下标(即 i mod 3 != 0)的列表。这个列表的顺序就是构造R时拼接的顺序。
  • index12rank12:一个大小为 N 的数组,对于 i 属于 B1∪B2,rank12[i]存储其在SA12中的排名(从0开始);对于 i 属于 B0,rank12[i]可以设为一个特殊值(如-1)。这个数组就是我们之前提到的rank数组,它是实现 O(1) 比较器的关键。

在递归调用返回SA_R后,我们需要根据SA_R来更新rank12SA_R中的每个值对应R的一个起始位置,而这个位置通过SA12数组映射回原字符串的下标i。我们遍历SA_R,按顺序为这些下标i分配新的、连续的排名。

3.2 基数排序的应用与优化

DC3算法中至少有两处用到基数排序,这是保证线性时间复杂度的关键。

  1. 在递归前,对三元组进行排序以离散化:我们构造的数组R包含了很多三元组。为了得到R'(离散化后的整数串),我们需要对所有三元组进行排序,以便给相同的三元组分配相同的排名。这里使用基于计数排序的基数排序是最合适的。因为每个“位”是一个字符(假设字符集大小有限,比如ASCII或字节),我们可以从第三字符、第二字符到第一字符,进行三轮计数排序(LSD,最低位优先)。这样能在 O(3 * (2N/3)) = O(N) 时间内完成排序和离散化。
  2. 在归并前,对 B0 后缀进行排序:排序 B0 后缀时,每个后缀的键值是(S[i], rank[i+1])。这同样是一个二元组,且每个分量的取值范围是有限的(字符集大小和排名大小)。我们可以使用两轮计数排序(先按第二关键字rank[i+1]排序,再按第一关键字S[i]排序,注意是MSD,最高位优先的思想,但实现时通常先排低位更稳定)。这也能在 O(|B0|) 时间内完成。

注意:基数排序的稳定性至关重要。在排序 B0 时,如果两个后缀的第一关键字S[i]相同,那么它们的相对顺序必须由第二关键字rank[i+1]决定。这就要求在按第一关键字排序时,必须使用稳定的排序算法。计数排序是稳定的,所以用它来实现基数排序是完美的。

3.3 比较器的正确实现:边界条件与细节

实现 B0 与 B12(B1∪B2)后缀的比较器compare函数是核心。伪代码如下:

def compare(i, j, S, rank12): # i 是 B0 下标, j 是 B1 或 B2 下标 if S[i] != S[j]: return S[i] < S[j] # 返回 True 如果 Suffix(i) < Suffix(j) # 第一个字符相等 if i % 3 == 0: # i 是 B0, i+1 是 B1 return rank12[i+1] < rank12[j+1] # 不应该出现 j 是 B0 的情况,因为我们在归并时是 B0 列表 vs B12 列表

对于两个 B0 后缀的比较,用于排序 B0 列表:

def compare_B0(i, j, S, rank12): # i, j 都是 B0 下标 if S[i] != S[j]: return S[i] < S[j] if S[i+1] != S[j+1]: return S[i+1] < S[j+1] # 前两个字符都相等 return rank12[i+2] < rank12[j+2] # i+2 是 B2

这里有一个极其关键的边界检查:当i+1,i+2,j+1,j+2可能超出字符串范围 N 时怎么办?例如,后缀本身就位于字符串末尾。我们的rank12数组对于 B0 下标是未定义的(设为-1),对于越界的下标更是没有意义。正确的处理方法是:

  • 在构造rank12数组时,我们不仅为 B1 和 B2 下标分配排名,还需要为虚拟的、越界的下标(即N,N+1,N+2)分配一个特定的、最小的排名(比如 -1 或 0,但必须保证它小于任何实际后缀的排名)。这样,在比较时,如果一个后缀已经结束(即遇到了哨兵$),那么它的“下一个字符”对应的排名就是这个最小的排名,比较逻辑依然成立。
  • 另一种常见实现技巧是,在原始字符串末尾显式地添加三个哨兵字符(如$$$),确保任何以i,i+1,i+2访问都不会越界。这样rank12数组只需要定义到 N+2 即可。

忽略边界检查是 DC3 算法实现中最常见的错误来源之一,会导致在比较接近末尾的后缀时产生错误结果或程序崩溃。

4. 实战剖析:以“yabbadabbado”为例,一步步推演DC3

让我们将理论付诸实践,用字符串S = "yabbadabbado$"(N=13) 来手动推演一遍 DC3 算法。我们将使用字符的 ASCII 码作为值,$的 ASCII 码我们设为 0(最小),a=97,b=98,d=100,o=111,y=121。

步骤1:初始化与分组字符串:y a b b a d a b b a d o $下标: 0 1 2 3 4 5 6 7 8 9 10 11 12 字符值:121 97 98 98 97 100 97 98 98 97 100 111 0

  • B0: i % 3 == 0 -> [0, 3, 6, 9, 12]
  • B1: i % 3 == 1 -> [1, 4, 7, 10]
  • B2: i % 3 == 2 -> [2, 5, 8, 11]

步骤2:构造新字符串 R(用于递归)我们按 B1 顺序后接 B2 顺序来收集三元组。对于每个下标 i,取(S[i], S[i+1], S[i+2]),不足的用 0 填充。

  • B1:
    • i=1: (97, 98, 98) ->(a, b, b)
    • i=4: (97, 100, 97) ->(a, d, a)
    • i=7: (98, 98, 97) ->(b, b, a)
    • i=10: (100, 111, 0) ->(d, o, $)(填充)
  • B2:
    • i=2: (98, 98, 97) ->(b, b, a)
    • i=5: (100, 97, 98) ->(d, a, b)
    • i=8: (98, 97, 100) ->(b, a, d)
    • i=11: (111, 0, 0) ->(o, $, $)(填充)

所以,三元组序列 R 为:[(a,b,b), (a,d,a), (b,b,a), (d,o,$), (b,b,a), (d,a,b), (b,a,d), (o,$,$)]。 为了递归,我们需要将每个三元组映射成一个整数(离散化)。我们先按字典序对这些三元组进行排序(可以用基数排序):

  1. (a,b,b)-> 排名 0
  2. (a,d,a)-> 排名 1
  3. (b,a,d)-> 排名 2
  4. (b,b,a)(来自 i=7) -> 排名 3
  5. (b,b,a)(来自 i=2) -> 排名 3 (相同,所以排名相同)
  6. (d,a,b)-> 排名 4
  7. (d,o,$)-> 排名 5
  8. (o,$,$)-> 排名 6

于是,新字符串 R' 为:[0, 1, 3, 5, 3, 4, 2, 6]。注意,这里有两个3,说明原问题中 B1∪B2 的后缀还没有完全区分开,需要递归。

步骤3:递归求解我们对 R' =[0, 1, 3, 5, 3, 4, 2, 6]递归构建后缀数组。递归过程遵循同样的 DC3 逻辑。为了简洁,我们假设递归调用正确返回了 R' 的后缀数组SA_RSA_R中的每个元素是 R' 的下标 (0-based),对应原字符串中 B1 或 B2 的起始位置。通过这个SA_R,我们可以得到 B1∪B2 后缀的排序顺序,并计算出rank12数组。rank12[i]表示后缀 i(i属于B1或B2)在 B1∪B2 中的排名。

假设递归后我们得到 B1∪B2 的排序顺序(按起始下标)是:[1, 4, 9, 11, 7, 2, 5, 8]? 等等,我们需要实际计算。由于递归过程较繁琐,我们这里直接给出一个合理的结果用于演示归并步骤。假设最终rank12数组如下(下标从0到12,B0位置为-1):rank12 = [-1, 0, 5, -1, 1, 6, -1, 4, 7, 2, -1, 3, -1]解读:后缀1(”abbadabbado$”)在B12中排第0;后缀4(”adabbado$”)排第1;后缀9(”do$”? 不对,9是B0,这里应该是后缀10? 检查:下标10是B1吗?10%3=1,是B1。但我们的列表里似乎没有10。看来我的假设rank12不完整。让我们重新构思一个正确且一致的例子。

为了不陷入过深的递归模拟,我们跳过递归的具体计算,直接假设一个合理的、排序后的 B12 下标列表以及对应的rank12。关键在于理解归并逻辑。

步骤4:排序 B0 后缀B0 下标为[0, 3, 6, 9, 12]。我们需要根据规则对它们排序。键值为(S[i], rank12[i+1])

  • i=0: (S[0]=121, rank12[1]=0) -> (121, 0)
  • i=3: (S[3]=98, rank12[4]=1) -> (98, 1)
  • i=6: (S[6]=97, rank12[7]=4) -> (97, 4)
  • i=9: (S[9]=97, rank12[10]=? 假设 rank12[10]=X) -> (97, X)
  • i=12: (S[12]=0, rank12[13] 越界,设为最小排名-1) -> (0, -1)

假设我们通过某种方式(比如基数排序)对这些键值排序后,得到 B0 的排序顺序。假设是[12, 9, 6, 3, 0](因为$最小,接着是a开头的,a相同则比 rank,最后是y)。

步骤5:归并 B0 和 B12现在我们有两个有序列表:

  • B0_sorted:[12, 9, 6, 3, 0]
  • B12_sorted: 假设是[1, 4, 10, 7, 2, 5, 8, 11](这是假设的B12排序结果)

我们使用双指针和compare函数进行归并。例如,比较Suffix(12)($) 和Suffix(1)(”abbadabbado$”)。显然$更小,所以12放入最终 SA。接着比较Suffix(9)Suffix(1)S[9]=’a’,S[1]=’a’,相等。由于9是 B0,调用compare(9, 1):比较(S[9]=’a’, rank12[10])(S[1]=’a’, rank12[2])。这取决于rank12[10]rank12[2]的值。假设rank12[10] < rank12[2],则Suffix(9)更小,9放入最终 SA。以此类推,直到所有后缀都被归并。

最终,我们得到完整的后缀数组 SA。对于”yabbadabbado$”,正确的后缀数组应该是(可以通过暴力方法验证):[12, 9, 6, 3, 1, 4, 10, 7, 0, 2, 5, 8, 11]对应后缀: 12:$9:do$6:abbado$3:badabbado$1:abbadabbado$4:adabbado$10:o$7:bbado$0:yabbadabbado$2:bbadabbado$5:dabbado$8:ado$11:o$? 这里下标11是o$,和下标10的o$重复了?不对,字符串是”yabbadabbado$”,下标10是’d’,后缀是”do$”;下标11是’o’,后缀是”o$”。我之前的字符串构造有误。”yabbadabbado”长度12,加$是13。下标10是’d’,11是’o’,12是’$’。所以后缀10是”do$”,后缀11是”o$”。那么上面的SA中,1011的位置可能需要调整。”do$””o$”比较,’d’(100) <’o’(111),所以10应该在11前面。我上面假设的B12排序[1, 4, 10, 7, 2, 5, 8, 11]是合理的。

这个推演过程虽然省略了递归的细节,但清晰地展示了 DC3 算法归并阶段如何利用已知的rank12信息,将 B0 和 B12 两个有序数组合并起来。在实际编程实现中,递归部分和归并部分都需要极其小心地处理下标映射和边界条件。

5. DC3 vs 倍增算法:场景选择与工程实践思考

既然有了 O(N log N) 的倍增算法,为什么还要选择更复杂的、理论复杂度更优的 DC3 算法?在实际工程中如何抉择?

性能对比

  • 时间复杂度:DC3 在最坏情况下是严格的 O(N),而倍增算法是 O(N log N)。对于极其庞大的 N(例如 > 1e8),DC3 的理论优势明显。
  • 常数因子:DC3 的常数因子通常比倍增算法大。因为它涉及递归、更多的数组分配和拷贝(构造 R 和 R‘)、以及更复杂的比较逻辑。对于中小规模的数据(例如 N < 1e6),倍增算法往往更快,因为它逻辑简单,缓存友好。
  • 空间复杂度:两者都可以做到 O(N) 的额外空间。但 DC3 在递归过程中需要创建大约 2N/3 大小的新数组,并且递归调用栈也有开销。倍增算法通常只需要几个大小为 N 的数组。因此 DC3 的常数空间开销通常也更大。

实现复杂度

  • 倍增算法:实现相对直观和简洁。核心是基数排序的循环,容易理解和调试。
  • DC3 算法:实现复杂得多。涉及模3分组、新字符串构造、递归调用、复杂的下标映射和边界处理。代码行数通常是倍增算法的两倍以上,调试难度也更高。

工程实践建议

  1. 默认选择倍增算法:在绝大多数实际应用中,字符串长度在百万级别以下,倍增算法完全够用,且实现简单,不易出错。例如,在大多数编程竞赛、常规文本处理工具中,倍增算法是首选。
  2. 考虑 DC3 的场景
    • 处理超长字符串,如基因组序列(长度可达数十亿碱基对)。
    • 对最坏情况性能有严格要求的库或框架。
    • 作为学术研究或教学,深入理解线性时间后缀数组构造的典范。
  3. 一个实用的混合策略:有些高性能库(如 libdivsufsort)采用了一种混合策略。它们可能先用倍增算法处理,当递归到子问题规模较小或者发现字符集很小时,切换到更简单的方法。或者,在实现 DC3 时,当递归到子串长度小于某个阈值(如 100)时,直接使用插入排序等简单方法,避免递归的额外开销。
  4. 内存与缓存考量:DC3 算法在递归过程中频繁创建新数组,可能对缓存不友好。在现代 CPU 架构下,倍增算法的顺序访问模式可能更能利用缓存预取,从而部分抵消其理论复杂度上的劣势。

从我个人的实现经验来看,第一次实现 DC3 算法时,几乎一定会遇到边界错误或排名计算错误。建议采取以下调试策略:

  • 从小数据开始:用长度小于10的字符串,手动计算每一步的中间结果,与程序输出对比。
  • 设计暴力验证:同时实现一个 O(N² log N) 的朴素后缀数组构建算法,用于验证 DC3 算法在小规模数据上的正确性。
  • 打印中间状态:在递归调用前后,打印出RR‘SA12rank12等关键数组,观察其是否符合预期。
  • 特别注意边界:单独测试以字符串末尾字符开头的后缀(即包含哨兵的后缀)的比较和排序是否正确。

最后,理解 DC3 算法的价值远不止于构建后缀数组。它展示了一种强大的算法设计范式:通过精心选择样本(模3余1和2的位置),将原问题归约为一个规模更小的相似子问题,然后利用子问题的解,通过精心设计的比较规则,在线性时间内解决剩余部分并完成合并。这种“分治-归并”的思想,以及利用“差分覆盖”来保证比较的可传递性,在其它字符串算法和数据结构中也有体现。虽然在实际中你可能不常需要手写 DC3,但彻底理解它,无疑会大大提升你对字符串算法本质的认知深度。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询