顺序统计树
概述
定义
顺序统计树
顺序统计树是一棵红黑树,其中每个节点 除红黑树的标准属性外,还额外维护一个域 ,定义为:
递归地:
其中,哨兵节点 的 为 。
size 属性的直观理解
对于一棵顺序统计树中的任意节点 :
- : 是叶子节点(左右子树为空)
- : 只有左子树
- : 只有右子树
生活化类比
想象一排按身高从矮到高站好的学生(这就是红黑树的中序遍历)。每个学生手里举着一块牌子,上面写着”以我为组长的小组一共有多少人”——这个人数就是 size。组长左边的人数就是左子树的 size,右边的人数就是右子树的 size。
如果你想知道”第5矮的学生是谁”,不需要从头数到第5个——你只需要看中间(根节点)组长的牌子:
- 如果组长左边有4个人,那组长就是第5矮的
- 如果组长左边有6个人,那第5矮的在左边那组里
- 如果组长左边只有3个人,那第5矮的在右边那组里,而且要找的是右组的”第1矮”(5 - 3 - 1 = 1)
核心性质
性质1:size 的维护开销为 O(1)
在红黑树的旋转操作中,只有两个节点的 size 需要更新,且每个更新仅需 时间。在插入和删除操作中,size 的维护被吸收到原有的 复杂度中。
性质2:OS-SELECT 的时间复杂度为 O(lg n)
OS-SELECT 从根节点出发,每层仅访问一个节点,因此最多访问 个节点,时间复杂度为 。
性质3:OS-RANK 的时间复杂度为 O(lg n)
OS-RANK 从目标节点出发,沿根的路径向上遍历,每层仅做 计算,因此时间复杂度为 。
性质4:保持红黑树的所有性质
顺序统计树仍然是合法的红黑树,所有红黑树的基本操作(SEARCH、INSERT、DELETE、MINIMUM、MAXIMUM、SUCCESSOR、PREDECESSOR)的时间复杂度不变。
OS-SELECT 操作
OS-SELECT:在以 为根的子树中,找到第 小的元素(即中序遍历中的第 个元素)。
OS-SELECT(x, i)
1 r = x.left.size + 1 // x 的秩(以 x 为根的子树中)
2 if i == r
3 return x // x 就是第 i 小的元素
4 elseif i < r
5 return OS-SELECT(x.left, i) // 在左子树中找第 i 小
6 else
7 return OS-SELECT(x.right, i - r) // 在右子树中找第 (i-r) 小
执行逻辑详解
- 计算当前节点的秩:,表示在以 为根的子树中, 是第 小的元素。
- 比较与递归:
- 若 ,则 就是答案,直接返回。
- 若 ,则第 小的元素在左子树中,递归到左子树继续查找第 小。
- 若 ,则第 小的元素在右子树中。由于左子树和 本身共占 个位置,所以在右子树中需要找的是第 小的元素。
示例演示
考虑以下顺序统计树(节点格式为 key[size]):
15[7]
/ \
6[3] 18[3]
/ \ / \
3[1] 7[1] 17[1] 20[1]
查找第 4 小的元素:OS-SELECT(root, 4)
| 步骤 | 当前节点 | 比较 | 动作 | |
|---|---|---|---|---|
| 1 | 15[7] | 返回 15 |
查找第 2 小的元素:OS-SELECT(root, 2)
| 步骤 | 当前节点 | 比较 | 动作 | |
|---|---|---|---|---|
| 1 | 15[7] | 4 | 递归到左子树, | |
| 2 | 6[3] | 2 | 返回 6 |
查找第 6 小的元素:OS-SELECT(root, 6)
| 步骤 | 当前节点 | 比较 | 动作 | |
|---|---|---|---|---|
| 1 | 15[7] | 4 | 递归到右子树, | |
| 2 | 18[3] | 2 | 返回 18 |
OS-RANK 操作
OS-RANK:求节点 在整棵树 的中序遍历中的秩(即 是第几小的元素)。
OS-RANK(T, x)
1 r = x.left.size + 1 // x 在以 x 为根的子树中的秩
2 y = x // 从 x 开始,沿路径向上遍历
3 while y != T.root
4 if y == y.p.right // 如果 y 是右孩子
5 r = r + y.p.left.size + 1 // 加上父节点及其左子树的大小
6 y = y.p // 继续向上
7 return r
执行逻辑详解
- 初始化:,这是 在以 为根的子树中的秩。
- 向上遍历:从 出发,沿着到根的路径向上走。每经过一个节点 :
- 如果 是其父节点的右孩子,说明父节点及其整个左子树中的所有元素都排在 前面,因此需要将 加到 上。
- 如果 是其父节点的左孩子,则不需要额外操作(父节点及右子树的元素都排在 后面)。
- 返回结果:遍历到根节点后, 就是 在整棵树中的秩。
示例演示
使用上面的同一棵树,求节点 18 的秩:OS-RANK
| 步骤 | 当前 | 是右孩子? | 的更新 | 的值 |
|---|---|---|---|---|
| 初始 | 18 | — | 1 | |
| 1 | 18 | 是(15的右孩子) | 5 | |
| 2 | 15 | 否(根节点,循环结束) | — | 5 |
所以节点 18 是第 5 小的元素。验证:中序遍历为 3, 6, 7, 15, 17, 18, 20,18 确实排在第 5 位。
size 的维护
旋转操作中的维护
在左旋和右旋中,只有两个节点的 size 需要更新:
LEFT-ROTATE(T, x)
...
// 标准左旋操作完成后,更新 size
y.size = x.size
x.size = x.left.size + x.right.size + 1
右旋类似,交换 和 的角色。
插入操作中的维护
在红黑树的插入过程中,从新插入的节点沿路径向上回溯,对路径上的每个节点执行:
由于路径长度为 ,每个更新为 ,总开销为 ,被吸收到插入的 复杂度中。
删除操作中的维护
在红黑树的删除过程中,从实际被删除节点的位置沿路径向上回溯,更新路径上每个节点的 size,同样为 。