平衡树
定义
平衡树是一种自平衡的二叉搜索树,它在进行插入和删除操作后能够自动调整其结构,以保持树的高度尽可能低,从而保证树的查找、插入和删除操作能够在对数时间内完成。最著名的平衡树有AVL树和红黑树。
-
AVL树:是一种严格的平衡树,任何节点的两个子树的高度最多相差1。因此,AVL树是最严格的平衡树之一,保证了树的平衡性,但这也意味着在进行插入和删除操作时可能需要较多的旋转操作来维持平衡。
-
红黑树:是一种比较宽松的平衡树,它保证了从根到任一叶节点的最长路径不超过最短路径的两倍,也就是说,树的高度至多是log(n+1),其中n是节点数。红黑树通过颜色标记节点,以及一系列的旋转和重新着色操作来保持平衡。
运用情况
- 数据库索引
- 文件系统的目录结构
- 编译器符号表
- 实现关联容器(如C++ STL中的
map
和set
) - 高效的优先级队列实现
注意事项
- 性能考量:虽然平衡树提供了O(log n)的时间复杂度,但频繁的旋转操作可能会导致较高的常数因子,影响实际性能。
- 内存使用:平衡树需要额外的空间来存储平衡信息(如颜色或高度),这会略微增加内存消耗。
- 实现复杂性:平衡树的插入和删除操作涉及到复杂的旋转和平衡调整,实现起来较为复杂。
- 非严格平衡:红黑树相比AVL树,虽然更宽松的平衡条件可能导致更高的树高,但在大多数情况下提供更好的平均性能。
解题思路
- 识别问题:确定问题是否需要在一个动态集合中进行快速查找、插入和删除操作,或者需要维护一个有序集合。
- 选择合适的平衡树:根据问题的具体要求选择AVL树或红黑树。如果极端平衡是关键,则选择AVL树;如果更关心操作的平均性能,则选择红黑树。
- 实现细节:
- 插入操作:从根节点开始,按照二叉搜索树规则向下遍历,直到找到合适的位置插入新节点。之后,根据所选平衡树的规则,进行必要的旋转和平衡调整。
- 删除操作:首先找到并删除目标节点,然后可能需要进行一系列的旋转和平衡调整,以保持树的平衡。
- 查找操作:从根节点开始,根据目标键值与当前节点键值的比较结果,递归地在左子树或右子树中查找。
- 测试与调试:编写测试用例,特别是针对边界条件和极端情况的测试,确保平衡树在各种操作下都能正确地调整其结构并保持平衡。
AcWing 253. 普通平衡树
题目描述
253. 普通平衡树 - AcWing题库
运行代码
#include <iostream>
#include <cstdlib>
#define N 100010
#define inf (int)1e9
using namespace std;
int n, x, op;
int root, idx;
struct TreapNode {
int l, r;
int val, key;
int size, cnt;
TreapNode() : l(0), r(0), val(0), key(0), size(0), cnt(0) {}
} tr[N];
// 更新节点的子树大小
void updateSize(int p) {
tr[p].size = tr[tr[p].l].size + tr[tr[p].r].size + tr[p].cnt;
}
// 获取新节点
int createNode(int key) {
tr[++idx].key = key;
tr[idx].val = rand();
tr[idx].cnt = tr[idx].size = 1;
return idx;
}
// 右旋操作
void rotateRight(int& p) {
int q = tr[p].l;
tr[p].l = tr[q].r;
tr[q].r = p;
p = q;
updateSize(p);
updateSize(tr[p].r);
}
// 左旋操作
void rotateLeft(int& p) {
int q = tr[p].r;
tr[p].r = tr[q].l;
tr[q].l = p;
p = q;
updateSize(p);
updateSize(tr[p].l);
}
// 构建初始的 Treap
void buildTreap() {
createNode(-inf);
createNode(inf);
root = 1;
tr[root].r = 2;
updateSize(root);
if (tr[1].val < tr[2].val) rotateRight(root);
}
// 插入节点
void insertNode(int& p, int key) {
if (p == 0) {
p = createNode(key);
return;
}
if (tr[p].key == key) {
tr[p].cnt++;
} else if (tr[p].key > key) {
insertNode(tr[p].l, key);
if (tr[tr[p].l].val > tr[p].val) rotateRight(p);
} else {
insertNode(tr[p].r, key);
if (tr[tr[p].r].val > tr[p].val) rotateLeft(p);
}
updateSize(p);
}
// 删除节点
void removeNode(int& p, int key) {
if (p == 0) return;
if (tr[p].key == key) {
if (tr[p].cnt > 1) {
tr[p].cnt--;
} else if (tr[p].l || tr[p].r) {
if (tr[p].r == 0 || tr[tr[p].l].val > tr[tr[p].r].val) {
rotateRight(p);
removeNode(tr[p].r, key);
} else {
rotateLeft(p);
removeNode(tr[p].l, key);
}
} else {
p = 0;
}
} else if (tr[p].key > key) {
removeNode(tr[p].l, key);
} else {
removeNode(tr[p].r, key);
}
updateSize(p);
}
// 通过值获取排名
int getRankByValue(int p, int key) {
if (p == 0) return 0;
if (tr[p].key == key) return tr[tr[p].l].size + 1;
if (tr[p].key > key) return getRankByValue(tr[p].l, key);
return tr[tr[p].l].size + tr[p].cnt + getRankByValue(tr[p].r, key);
}
// 通过排名获取值
int getValueByRank(int p, int rank) {
if (p == 0) return inf;
if (tr[tr[p].l].size >= rank) return getValueByRank(tr[p].l, rank);
if (tr[tr[p].l].size + tr[p].cnt >= rank) return tr[p].key;
return getValueByRank(tr[p].r, rank - tr[tr[p].l].size - tr[p].cnt);
}
// 获取前驱
int getPredecessor(int p, int key) {
if (p == 0) return -inf;
if (tr[p].key >= key) return getPredecessor(tr[p].l, key);
return max(tr[p].key, getPredecessor(tr[p].r, key));
}
// 获取后继
int getSuccessor(int p, int key) {
if (p == 0) return inf;
if (tr[p].key <= key) return getSuccessor(tr[p].r, key);
return min(tr[p].key, getSuccessor(tr[p].l, key));
}
int main() {
buildTreap();
cin >> n;
while (n--) {
cin >> op >> x;
if (op == 1) insertNode(root, x);
else if (op == 2) removeNode(root, x);
else if (op == 3) cout << getRankByValue(root, x) - 1 << endl;
else if (op == 4) cout << getValueByRank(root, x + 1) << endl;
else if (op == 5) cout << getPredecessor(root, x) << endl;
else cout << getSuccessor(root, x) << endl;
}
return 0;
}
代码思路
-
定义TreapNode结构体:
- 每个节点包含左子树指针
l
和右子树指针r
。 val
是节点的优先级,由随机数生成,用于平衡树。key
是节点存储的实际值。size
是节点子树的节点总数。cnt
是节点的重复计数,用于处理具有相同键值的元素。
- 每个节点包含左子树指针
-
辅助函数:
updateSize
用于更新节点的子树大小。createNode
用于创建新节点。rotateRight
和rotateLeft
分别执行右旋和左旋操作,以保持树的平衡。buildTreap
初始化Treap,插入最小值和最大值节点作为边界。
-
主要操作:
insertNode
用于插入新节点,根据优先级和键值进行插入和旋转操作。removeNode
用于删除节点,考虑到节点的重复计数和平衡调整。getRankByValue
返回给定键值的排名。getValueByRank
返回给定排名的键值。getPredecessor
和getSuccessor
分别返回给定键值的前驱和后继。
-
主函数流程:
- 初始化Treap。
- 循环读取操作指令和参数。
- 根据操作类型执行相应的函数调用。
- 输出结果。
改进思路
-
随机数生成: 当前的代码使用
rand()
函数生成随机数作为节点的优先级,但rand()
在某些情况下可能不是最优选择,因为它需要调用系统函数且可能有周期性问题。可以改为使用更高效的伪随机数生成器,例如C++11中的std::mt19937
。 -
分离接口与实现: 将Treap的操作接口(如插入、删除、查询等)与具体实现分离,可以使代码更模块化,易于维护和扩展。例如,可以创建一个Treap类,将所有相关的数据成员和方法封装在一起。
-
异常处理: 添加适当的错误检查和异常处理机制,比如在插入不存在的节点时抛出异常,或者在操作失败时给出明确的错误信息。
-
代码简化: 有些操作可以通过简化逻辑来提高效率。例如,在删除节点时,如果一个节点只有单边子树,可以直接用子树替换该节点,避免不必要的旋转。
-
性能优化: 对于大量操作的场景,可以考虑使用懒惰更新技术来延迟更新
size
字段,直到真正需要它为止。这样可以减少每次操作时的计算量。 -
并行化: 如果Treap将用于多线程环境,可以考虑添加锁或其他同步机制来保证线程安全,或者设计一种无锁的并发Treap。
-
序列化与持久化: 实现Treap的序列化和反序列化功能,使其能够保存到磁盘或网络传输,以便于持久化存储和远程调用。
-
测试与调试: 增加单元测试来验证每个功能的正确性,以及压力测试来确保Treap在大数据集下的稳定性和性能。
-
文档与注释: 为代码添加详细的注释和文档,说明每个函数的目的、参数和返回值,这将帮助其他开发者理解和使用你的Treap实现。
-
功能扩展: 考虑增加更多的功能,如区间查询、区间修改等,这将使Treap成为一个更强大的数据结构。