Treap引入
前置知识
treap是由BST和heap组合而成的数据结构,这一点也体现在它的名字上:treap=tree+heap
BST中,每个节点的左儿子都比它小,右儿子都比它大,可以实现有序的遍历,但是可能因为数据特殊的排列方式,而退化为线性
heap中,每个父节点都是当前子树内权值最大(或最小)的点。
在BST的基础上加一个控制树深度的功能,就得到了一个简单的平衡树。
与AVL的人为控制深度不同,Treap借由heap的性质,来实现对树深度的压缩。
引理
一棵随机生成的二叉树,其期望深度是logN (证明略)
(因为Treap的实现依赖此引理,所以在某些极端情况下时间复杂度有可能退化,但是可能性非常非常小)
实现(不使用指针的写法)
结构体声明
struct Node { int l, r; int key, val; int cnt, size; }tr[N];
其中,key键值是真实存在的,存储在BST当中的数值,即我们会用到的有效值;而val则是用于辅助进行堆排序的,我们生成的一个值。
那么,这个val的值应该取多少呢?根据引理,我们希望这棵树的高度控制在logN级别,那么,我们可以在每个节点,给val赋一个随机值,这样一来,通过使用堆的更新方式,我们就能使平衡树的高度维持在随机二叉树的水平,即logN级别。
新建节点
int get_node(int key) { tr[ ++ idx].key = key; tr[idx].val = rand(); tr[idx].cnt = tr[idx].size = 1; return idx; }
这里的idx即index,用于指示节点的序号,相当于一个指针。
基础操作——pushup
void pushup(int p) { tr[p].size = tr[tr[p].l].size + tr[tr[p].r].size + tr[p].cnt; }
pushup(p)的作用是通过p的两个子节点,来更新p节点的信息。
建树
void build() { get_node(-INF), get_node(INF); root = 1, tr[1].r = 2; pushup(root); if (tr[1].val < tr[2].val) zag(root); }
通过预先加入两个key值为正负无穷的节点,我们可以保证之后会插入的所有点都在这两点之间,避免了一些特判的麻烦。
zig,zag,右旋左旋
void zig(int &p) // 右旋 { int q = tr[p].l; tr[p].l = tr[q].r, tr[q].r = p, p = q; pushup(tr[p].r), pushup(p); } void zag(int &p) // 左旋 { int q = tr[p].r; tr[p].r = tr[q].l, tr[q].l = p, p = q; pushup(tr[p].l), pushup(p); }
平衡树的基本操作,也是核心操作
插入新元素
void insert(int &p, int key) { if (!p) p = get_node(key); else if (tr[p].key == key) tr[p].cnt ++ ; else if (tr[p].key > key) { insert(tr[p].l, key); if (tr[tr[p].l].val > tr[p].val) zig(p); } else { insert(tr[p].r, key); if (tr[tr[p].r].val > tr[p].val) zag(p); } pushup(p); }
这里的cnt表示每个节点元素出现的次数,防御式编程,以处理输入中存在多个相同元素的情况。
删除元素
void remove(int &p, int key) { if (!p) 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 || tr[tr[p].l].val > tr[tr[p].r].val) { zig(p); remove(tr[p].r, key); } else { zag(p); remove(tr[p].l, key); } } else p = 0; } else if (tr[p].key > key) remove(tr[p].l, key); else remove(tr[p].r, key); pushup(p); }
众所周知,在BST中,要删除一个内部的节点比较麻烦,我们可以借助平衡树的性质,先把要删除的节点旋转成为叶节点,然后再执行删除操作。
更为简便的做法是,为要删除的节点打上懒标记lazy tag,因为我们的代码中已经每个节点已经有cnt这个成员,我们可以用cnt是否为零来表示lazy tag是否为true。
fhq-treap
【数据结构】FHQ Treap 详解 - ctjcalc - 博客园 (cnblogs.com) 可以去大佬的链接进一步了解Treap的变体喵