← 返回题目列表

平衡树中的 split 和 merge 操作有什么用?

高频 困难 第 12 / 25 题 更新于 2026/07/29
平衡树splitmergeTreapSplay区间操作

简化版

split 是把一棵平衡树按 key 或排名拆成两棵树,merge 是把两棵有序且互不交叉的树合并成一棵。它们常用于 Treap、Splay 等结构,实现区间删除、区间翻转、排名查询、动态序列维护等操作。前提是拆分和合并后仍要保持 BST 顺序和平衡信息。

详细版

按 key split 时,通常把树拆成 <= x> x 两部分;按排名 split 时,把前 k 个元素拆成一棵树,剩余元素拆成另一棵树。merge 的前提是左树所有 key 都小于右树所有 key,然后根据平衡树规则把两棵树接起来,例如 Treap 按 priority 决定谁做根。

split/merge 的价值在于把复杂区间操作转化成几次树操作。例如删除区间 [L, R],可以先 split 出 < L>= L,再从后者 split 出 <= R> R,丢掉中间树,再 merge 两边。只要底层结构高度 O(log n),这些操作通常也是 O(log n) 期望或摊还复杂度。

完整版教学

一、为什么平衡树需要 split 和 merge

普通 BST 的接口通常是 search、insert、delete。但在很多面试和竞赛风格题里,需要维护一个动态有序集合或动态序列,例如“删除排名 10 到 20 的元素”“把区间翻转”“按 key 切开后批量移动”。如果只用单点删除,会把区间操作做成 O(k log n)。split 和 merge 让整段区间能作为一棵子树被整体拿出来。

目标:删除 [L, R]
不使用 split/merge:
  删除 L, L+1, ..., R 逐个处理

使用 split/merge:
  拆出左边、中间、右边
  丢掉中间
  合并左边和右边

这就是 split/merge 的意义:它把“多个元素的操作”提升成“几棵树的操作”。

二、按 key split 的语义

按 key split 通常定义为:给定树 root 和边界 x,把树拆成 A、B 两棵,A 中所有 key <= x,B 中所有 key > x。拆分过程中必须保持每棵树内部仍是合法 BST。

原序列: [1, 3, 5, 7, 9]
split by x=5
A = [1, 3, 5]
B = [7, 9]

在 Treap 中,split 可以递归写:如果 root.key <= x,root 应该属于左树,那么递归拆 root.right;如果 root.key > x,root 应该属于右树,那么递归拆 root.left。递归返回后重新接孩子并更新 size。

三、按排名 split 的语义

按排名 split 更适合动态序列。它把前 k 个元素拆成 A,剩下的拆成 B。这里不一定依赖 key 的数值大小,而是依赖每个节点维护的 size

操作输入输出
splitByRank(root, 3)[10,20,30,40,50]A=[10,20,30], B=[40,50]
splitByRank(root, 0)[10,20]A=[], B=[10,20]
splitByRank(root, 5)[1,2,3,4,5]A=[1,2,3,4,5], B=[]

按排名拆分的关键还是左子树大小。如果左子树大小已经大于等于 k,就继续在左边拆;否则当前节点和左子树都属于前 k 个的一部分,要去右边拆剩余数量。

四、merge 的前提和实现直觉

merge 不是随便把两棵树接起来。它要求左树所有 key 都小于右树所有 key,否则 BST 顺序会被破坏。满足这个前提后,Treap 可以按 priority 决定根:谁 priority 更高,谁做合并后根,再递归合并另一边。

Node merge(Node a, Node b) {
    if (a == null) return b;
    if (b == null) return a;
    if (a.priority < b.priority) {
        a.right = merge(a.right, b);
        pull(a);
        return a;
    } else {
        b.left = merge(a, b.left);
        pull(b);
        return b;
    }
}

这里用小根堆 priority。pull 表示重新计算 size、sum 等增强信息。忘记 pull 会导致后续排名、区间和全部错。

五、区间操作如何拆成三段

假设要删除有序集合中 key 位于 [L, R] 的节点。可以做两次 split:

(A, B) = split(root, L - 1)
(C, D) = split(B, R)

A: key < L
C: L <= key <= R
D: key > R

删除区间 = merge(A, D)

如果是按排名删除第 3 到第 5 个元素,也一样:

(A, B) = splitByRank(root, 2)
(C, D) = splitByRank(B, 3)
root = merge(A, D)

第二次拆 B 时用长度 3,是因为第 3 到第 5 个一共有 3 个元素。这个数字例子能帮助避免排名区间的 off-by-one 错误。

六、常见误区与追问

记忆钩子:区间操作先切三段,中间处理完,再把两边接回去。

  • 误区:merge 可以合并任意两棵 BST。 必须保证左树所有 key 小于右树所有 key,否则合并后不再是 BST。
  • 误区:split 后不需要更新 size。 拆分会改变孩子指针,所有受影响节点的 size、sum、lazy 标记都要维护。
  • 误区:按 key split 和按排名 split 是一回事。 前者按值域切,后者按中序位置切,依赖的信息不同。
  • 追问:split/merge 常在哪些树里用? Treap 和 Splay 很常见,尤其是 FHQ Treap、文艺平衡树这类动态序列题。
  • 追问:复杂度是多少? 取决于底层树高,Treap 通常是期望 O(log n),Splay 通常是摊还 O(log n)。
  • 追问:如何支持区间翻转? 拆出中间树后打 lazy reverse 标记,再 merge 回去,期间 pushdown 维护懒标记。

七、加强记忆

split 和 merge 是平衡树处理区间问题的基础拼装件。split 把一棵树按 key 或排名拆开,merge 在左右 key 范围不交叉的前提下重新接起来。删除、翻转、区间统计都可以拆成“左段、中段、右段”三棵树,中段处理完再合并。回答这题时要特别强调两个红线细节:merge 必须满足有序前提,split/merge 后必须更新 size、sum 或 lazy 标记,否则结构看似连上了,增强信息已经坏了。