平衡树学习笔记
二叉搜索树
平衡树的基础。
二叉搜索树是一种二叉树,并且满足左儿子的值小于根的值,右儿子的值大于根的值。
并且二叉搜索树的子树也是二叉搜索树。
基础操作:
先来规定一下变量名:
表示以 为根的子树大小, 表示 的左儿子, 表示 的右儿子。
表示 节点的权值, 表示 节点权值的数量。
遍历操作:
如果没有儿子了直接返回,然后遍历左右儿子输出。
void print(int now) { if(!now) return; print(lc[now]); for(int i = 1; i <= cnt[now]; i++) printf("%d ", val[now]); print(rc[now]); }
查找最值操作:
因为左儿子是最小的,我们从根节点出发,一直找左儿子,最终找到的左儿子所代表的权值就是最小值。
因为右儿子是最小的,我们从根节点出发,一直找右儿子,最终找到的右儿子所代表的权值就是最大值。
int findMin(int now) { if(!lc[now]) return now; return findMin(ls[now]); } int findMax(int now) { if(!rc[now]) return now; return findMax(rs[now]); }
插入操作:
考虑从根节点出发,找属于他的位置。
如果当前没有这个节点,说明需要新建节点。
如果有的话,就根据二叉搜索树的性质。
如果 比当前的值小,去左儿子找。
如果 跟当前的值相等了,个数加 ,然后返回。
如果 比当前的值大,去右儿子找。
void Insert(int &now, int v) { if(!now) { now = ++idx; val[now] = v, cnt[now] = siz[now] = 1; return; } siz[now]++; if(val[now] > v) Insert(ls[now], v); if(val[now] == v) return cnt[now]++, void(); if(val[now] < v) Insert(rs[now], v); }
删除操作:
如果是删除最值操作。
如果是删除最小值,就是一直遍历到最后,如果说是叶子节点,就直接删除。
如果不是叶子节点,因为是找最小值再删除,我们先一直遍历左儿子,到了没有左儿子的节点,将这个节点删除就是将这个节点直接变成自己的右儿子。
删除最大值同理。
int deletemin(int &now) { if(!ls[now]) { int u = now; now = rs[now]; return u; } else { int u = deletemin(lc[now]); siz[now] -= cnt[u]; return u; } } int deletemax(int &now) { if(!rs[now]) { int u = now; now = ls[now]; return u; } else { int u = deletemax(rc[now]); siz[now] -= cnt[u]; return u; } }
如果是随便删除一个权值为 的点呢。
我们从根节点出发,根据二叉搜索树的性质,去找到 所在的位置。
如果这个地方 直接减 ,然后返回。
否则如果这个节点只有一个儿子,那么直接加这个节点设成他的儿子。
如果有两个儿子,就将这个节点变成左儿子的最大值或者右儿子的最小值。
void Del(int &now, int v) { siz[now]--; if(val[now] == v) { if(cnt[now] > 1) { cnt[now]--; return; } if(ls[now] && rs[now]) now = deletemin(rs[now]); else now = ls[now] + rs[now]; return; } if(val[now] > v) Del(ls[now], v); if(val[now] < v) Del(rs[now], v); }
求排名操作:
从根节点出发,根据权值不断的找即可。
求元素排名:
int Rank(int now, int v) { if(val[now] == v) return siz[ls[now]] + 1; if(val[now] > v) return Rank(ls[now], v); if(val[now] < v) return Rank(rs[now], v) + siz[ls[now]] + cnt[now]; }
求排名的元素:
int kth(int now, int k) { if(siz[ls[now]] >= k) return kth(ls[now], k); if(siz[ls[now]] < k - cnt[now]) return kth(rs[now], k - siz[ls[now]] - cnt[now]); return val[now]; }
Splay
全名伸展树。
是一种通过玄学旋转来实现树的平衡的树。
反正复杂度是对的。
基础操作:
旋转操作:
我们要让 当 的爹,但是又要满足二叉搜索树的性质。
现在 是 的左儿子,所以 。
所以 想当爹的话 就要成为 的右儿子。
但是 的右儿子上已经有人了。
于是我们考虑 的关系: 。
的爹就变成了 。
所以我们直接把 的左儿子变成 ,这样就完成了旋转。
大概总结一下这个规律:
是 的爹, 是 的爹。
如果说 是 的 号儿子。
如果说 说明是右儿子, 就是左儿子。
如果 是左儿子,说明了 要当 的右儿子。
如果说 是右儿子,说明了 要当 的左儿子。
- 所以说 总是会当 的 号儿子。
然后 的 号儿子因为 的到来,要变成 的 号儿子。
最后更新父节点的信息,更新旋转后节点的子树大小即可。
void moveroot(int x) { int y = tree[x].fa; int z = tree[y].fa; int k = (tree[y].ch[1] == x); // x 是 y 的哪个儿子 tree[z].ch[tree[z].ch[1] == y] = x; // 更新 z 的儿子是 x tree[x].fa = z; // x 的爹是 z tree[y].ch[k] = tree[x].ch[k ^ 1]; // x 的原来要变成 y 的那个儿子成为了 y 的儿子。 tree[tree[x].ch[k ^ 1]].fa = y; // 更新爹 tree[x].ch[k ^ 1] = y; // y 成为了 x 的子树 tree[y].fa = x; // y 的爹是 x }
如果你觉得这样就能使树平衡就大错特错了,还是有数据可以hack的。
依旧是上面那个图。
按照要求不断的旋转 节点。
然后继续旋转 节点。
然后你就发现,这不是寄了,旋转完之后还是一条链。
本身上是因为让 不断的旋转,结果 是一条链,然后不断的旋转只是把 移动他的同一个儿子罢了。
所以我们先转一下 节点,然后不断的去转 就行了。
先旋转 节点。
然后旋转 节点。
void splay(int x, int goal) { while(tree[x].fa != goal) { // 当他的爹不是目标节点的时候 int y = tree[x].fa, z = tree[y].fa; if(z ^ goal) // 为啥防止转两遍然后把 x 直接转到目标 (tree[z].ch[1] == y) ^ (tree[y].ch[1] == x) ? moveroot(x) : moveroot(y); // 判断是否是链 moveroot(x); } if(!goal) root = x; // 更新根节点 }
查找操作:
我们从根节点出发,如果树是空的直接返回。
否则去找那个儿子,根据权值的大小去找儿子。
最后为了后面的求前驱后继操作,把找到的这个节点转到根节点。
void find(int x) { int u = root; if(!u) return; // 如果是空树 while(tree[u].ch[x > tree[u].val] && x != tree[u].val) // 找符合条件的儿子跳过去 u = tree[u].ch[x > tree[u].val]; splay(u, 0); // 旋转到根节点,维护树的平衡 }
插入操作:
我们从根节点出发,找到他应该插入的位置。
如果这个位置存在, 加 ,然后返回。
如果说不存在,新建一个节点。
void Insert(int x) { int u = root, fa = 0; while(u && tree[u].val != x) fa = u, u = tree[u].ch[x > tree[u].val]; if(u) tree[u].cnt++; else { u = ++idx; if(fa) tree[fa].ch[x > tree[fa].val] = u; tree[idx].fa = fa, tree[idx].val = x, tree[idx].cnt = 1, tree[idx].siz = 1; } splay(u, 0); }
前驱后继操作:
先找到这个位置,但是不一定能直接找到,先查找这个位置,然后把他变成根,这样前驱就是左子树最大值。
后继就是右子树最小值。
int Next(int x, int f) { find(x); int u = root; if(tree[u].val > x && f) return u; if(tree[u].val < x && !f) return u; u = tree[u].ch[f]; while(tree[u].ch[f ^ 1]) u = tree[u].ch[f ^ 1]; return u; }
删除操作:
首先找到这个数的前驱,把他Splay到根节点。
然后找到这个数后继,把他旋转到前驱的底下。
比前驱大的数是后继,在右子树。
比后继小的且比前驱大的有且仅有当前数。
在后继的左子树上面。
因此直接把当前根节点的右儿子的左儿子删掉就可以啦。
void Delete(int x) { int last = Next(x, 0); int nxt = Next(x, 1); splay(last, 0), splay(nxt, last); int del = tree[nxt].ch[0]; if(tree[del].cnt > 1) { tree[del].cnt--, splay(del, 0); } else tree[nxt].ch[0] = 0; }
FHQ-Treap
码量短的平衡树,几分钟就能写完,并且特别好调,推荐学习这种。
split 操作:
分裂操作,按值 分裂,分裂成两颗子树 和 , 上的值都 , 上的值都 。
从根节点开始,如果当前节点的权值 ,说明他的左子树也 ,但是右子树可能有一部分 ,所以去递归处理右子树,同时为了找到分裂后真正的 的右儿子。
值 的时候一样。
inline void split(int p, int v, int &x, int &y) { if(!p) {x = y = 0; return;} if(tr[p].val <= v) x = p, split(rs(x), v, rs(x), y); else y = p, split(ls(y), v, x, ls(y)); pushup(p); }
merge 操作:
分裂操作的逆操作,将两颗子树按照随机的 值合并起来,如果 ,按照小根堆, 需要在 的上面,因为 的权值小于 的权值,所以只需要把 与 合并起来当做 的右儿子就行了。
inline int merge(int x, int y) { if(!x || !y) return x + y; if(tr[x].key < tr[y].key) { rs(x) = merge(rs(x), y); pushup(x); return x;} else {ls(y) = merge(x, ls(y)), pushup(y); return y; } }
insert 操作:
插入一个权值为 的点,先按权值 分裂成 ,然后新建一个权值为 的节点,把 合并起来,再和 合并。
inline void Insert(int v) { split(root, v, x, y); z = newnode(v); root = merge(merge(x, z), y); }
delete 操作:
删除一个权值为 的点,先按权值 分裂成 ,再把 按照权值 分裂成 ,这个时候 的值全部等于 ,将 的根删除掉,也就是直接将 的 左右儿子合并起来。
inline void del(int v) { split(root, v, x, z), split(x, v - 1, x, y); y = merge(ls(y), rs(y)); root = merge(merge(x, y), z); }
getrank 操作:
找权值为 的点的排名,也就是比他小的数的个数再加 ,按照 分裂成 , 子树上的都是比他小的,再加一即为答案。
inline int getrank(int v) { split(root, v - 1, x, y); res = tr[x].siz + 1; root = merge(x, y); return res; }
getnum 操作:
找排名为 的点的权值,从根节点出发,如果 左子树的大小,说明了在左子树上,去左子树找,如果左子树大小加一等于 ,说明当前点为要找的点,否则去右子树找,要减去左子树大小和根节点的大小。
inline int getnum(int p, int k) { if(tr[ls(p)].siz >= k) return getnum(ls(p), k); else if(tr[ls(p)].siz + 1 == k) return p; else return getnum(rs(p), k - tr[ls(p)].siz - 1); }
getpre 操作:
按照权值 分裂成 ,找到 上最大的数。
inline int getpre(int v) { split(root, v - 1, x, y), res = getnum(x, tr[x].siz), root = merge(x, y); return tr[res].val; }
getnxt 操作:
按照权值 分裂成 ,找到 上最小的数。
inline int getnxt(int v) { split(root, v, x, y), res = getnum(y, 1), root = merge(x, y); return tr[res].val; }
本文作者:TLE_Automation
本文链接:https://www.cnblogs.com/tttttttle/p/16594240.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)