Splay
伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由丹尼尔·斯立特Daniel Sleator 和 罗伯特·恩卓·塔扬Robert Endre Tarjan 在1985年发明的
在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法, 在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。
1、时间局部性和空间局部性的原理
(1)刚刚被访问的元素,极有可能在不久后再次被访问
(2)刚刚被访问的元素,它的相邻节点也很有可能被访问
伸展树的实现更为便捷,它无须时刻保持全树平衡,任意节点的左右子树高差无限制。伸展树的单次搜索也可能需要n次操作,但可以在任意足够长的真实操作序列中保持均摊意义上的高效率。伸展树可以保证m次连续搜索操作的复杂度为,而不是。伸展树的优势在于不需要记录平衡因子、树高、子树大小等额外信息,所以适用范围更广,对m次连续搜索操作具有较高的效率。
考虑到局部性原理,伸展树会在每次操作后都将被访问的节点旋转至树根,加速后续的操作。当然,旋转前后的搜索树必须相互等价。这样,查询频率高的节点应当经常处于靠近树根的位置。旋转的巧妙之处在于不打乱数列中数据大小的关系(中序有序性)的情况下,所有操作的均摊复杂度仍为O(mlogn)。
下面一起来看看Splay是如何实现的吧!
首先要熟悉的是左旋和右旋操作,普通treap中是左旋和右旋分开来的
分别是右旋zig
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); }
左旋zag
void zag(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); }
经过这些年大佬们的努力,把左右旋操作并为一个函数
左右旋rotate(最好自己画图理解一下)
void rotate(int x) { int y = tr[x].p, z = tr[y].p;//父亲节点 int k = tr[y].s[1] == x; // 判断x是y的左儿子还是右儿子,如果是左儿子返回0,右儿子返回1 tr[z].s[tr[z].s[1] == y] = x, tr[x].p = z; //同上 tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y; tr[x].s[k ^ 1] = y, tr[y].p = x; }
关键函数Splay
void splay(int x, int k) //将x旋转到节点k的下面 (注意千万不能改变旋转的顺序,否则不能保证logn的复杂度) { while(tr[x].p != k) // 如果x的父节点不是k,继续旋转 { int y = tr[x].p, z = tr[y].p; if(z != k)//如果z不是k的话,就旋转两次,否则旋转一次 if((tr[z].s[1] == y) ^ (tr[y].s[1] == x)) rotate(x); //如果是折线的话旋转两次x else rotate(y);//否则就旋转y,再旋转x rotate(x); } if(!k) root = x; //如果k等于0,x是根节点 }
模板题:https://www.acwing.com/problem/content/2439/
代码展示:
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 100010; int n, m; struct Node{ int s[2], val, p; int sz, lazy; void init(int _v, int _p){ val = _v; p = _p; } }tr[N]; int root, idx; void pushup(int u) { tr[u].sz = tr[tr[u].s[0]].sz + tr[tr[u].s[1]].sz + 1; } void pushdown(int u) { if(tr[u].lazy) { swap(tr[u].s[0], tr[u].s[1]); tr[tr[u].s[0]].lazy ^= 1; tr[tr[u].s[1]].lazy ^= 1; tr[u].lazy = 0; } } void rotate(int x) { int y = tr[x].p, z = tr[y].p; int k = tr[y].s[1] == x; tr[z].s[tr[z].s[1] == y] = x, tr[x].p = z; tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y; tr[x].s[k ^ 1] = y, tr[y].p = x; pushup(y), pushup(x); } void splay(int x, int k) { while (tr[x].p != k) { int y = tr[x].p, z = tr[y].p; if (z != k) if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x); else rotate(y); rotate(x); } if (!k) root = x; } void insert(int v) { int u = root, p = 0; while(u) p = u, u = tr[u].s[v > tr[u].val]; u = ++ idx; if(p) tr[p].s[v > tr[p].val] = u; tr[u].init(v, p); splay(u, 0); } int get_k(int k) { int u = root; while(true) { pushdown(u); if(tr[tr[u].s[0]].sz >= k) u = tr[u].s[0]; else if(tr[tr[u].s[0]].sz + 1 == k) return u; else k -= tr[tr[u].s[0]].sz + 1, u = tr[u].s[1]; } return -1; } void ldr(int u) { if(!u) return; pushdown(u); ldr(tr[u].s[0]); if(tr[u].val >= 1 && tr[u].val <= n) printf("%d ", tr[u].val); ldr(tr[u].s[1]); } int main() { cin >> n >> m; for(int i = 0; i <= n + 1; i ++ ) insert(i); while(m -- ) { int l, r; cin >> l >> r; int L = get_k(l), R = get_k(r + 2); splay(L, 0), splay(R, L); tr[tr[R].s[0]].lazy ^= 1; } ldr(root); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话