前言
Splay(伸展树)是一种灵活多变的高级数据结构,可以很方便的执行各种动态的区间操作。由丹尼尔·斯立特Daniel Sleator和罗伯特·恩卓·塔扬Robert Endre Tarjan在1985年发明。
Splay是一颗二叉搜索树,它建立在二叉搜索树(BST)之上,当然也是平衡树的一种,下面简单介绍一下BST与平衡树:
首先介绍BST,也就是所有平衡树的开始,他的名字是二叉查找树.
给定一棵二叉树,每一个节点有一个权值,命名为关键码,而BST性质就是,对于树中任何一个节点,都满足一下性质:
- 这个节点的关键码不小于它的左子树上,任意一个节点的关键码
- 这个节点的关键码不大于它的右子树上,任意一个节点的关键码
然后我们就可以发现这棵树的中序遍历,就是一个关键码单调递增的节点序列,说的直白点,就是一个排好序的数组(注意是非严格单调递增序列)。
我们知道,在一颗查找二叉树(BST)中,插入过程的均摊时间复杂度为O(logn),但是在一些特殊情况,BST树会退化成一条链,导致时间复杂度从O(logn)退化到O(n),这是不可以的,所以产生了一种数据结构——平衡树
。
平衡树最主要的功能就是旋转
,通过等价的旋转,可以最优化BST的深度,例如下面的两棵BST树的对比,他们实质上是等价的,但是第一颗不平衡,操作时会造成不必要的时间浪费。
当然除此之外,平衡树还有以下功能(来自平衡树 - 维基百科 ):
- 旋转(rotate):几乎所有平衡树的操作都基于树旋转 操作(也有部分基于重构,如替罪羊树 ),通过旋转操作可以使得树趋于平衡。对一棵查找树(searchtree)进行查询、新增、删除等动作,所花的时间与树的高度成比例,并不与树的容量成比例。如果可以让树维持平衡,也就是让高度维持在O(logn)左右,就可以在O(logn)的复杂度内完成各种基本操作。
- 插入(insert):在树中插入一个新值。
- 删除(delete):在树中删除一个值。
- 查询前驱(predecessor):前驱定义为小于x,且最大的数。
- 查询后继(successor):后继定义为大于x,且最小的数。
如果维护同时节点大小(size),还可以支持以下操作:
- 查询排名(rank):排名定义为比x小的数的个数加一。
- 查询第k大:即排名为k的数。
那么实际上Splay树(伸展树)的原理是基于类似程序局部性原理的假设:一个节点在一次被访问后,这个节点很可能不久再次被访问。Splay树的做法就是在每次一个节点被访问后,我们就把它推到树根的位置。正像程序局部性原理的实际效率被广泛证明一样,Splay树在实际的搜索效率上也是非常高效的。尽管存在最坏情况下单次操作会花费O(n)的时间,但是这种情况并不是经常发生,而实际证明伸展树能够保证m次连续操作最多花费O(mlogn)的时间。
Splay
所有平衡树的思路都是维护一颗BST树的平衡状态
,也就是不改变中序遍历结果的前提之下,尽可能减少Splay树的深度。
前面我们说过了,Splay的主要思想是:对于查找频率较高的节点,使其处于离根节点相对较近的节点。当然我们统计每一个点的查找次数是不现实的,我们可以理解每一次被查到的节点频率比较高,说白了就是你把每次查找到的点搬到根节点去,这样就可以保证查找的效率。
每次移动的过程就是旋转的过程(rotate),在Splay中有两种旋转,分别叫做单旋
与双旋
,我们将他们设计为rotate函数与splay函数。在设计之前,我们需要先定义一颗BST树,如下:
因为OI多是面向过程设计,下列写法并不在生产环境适用,若要应用于生产环境中,请使用Class+指针面向对象设计BST树。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| struct Node { int fa; int val; int cnt; int size; int ch[2]; Node() { fa = 0; val = 0; cnt = 0; size = 0; ch[0] = ch[1] = 0; } Node(int Fa, int Val, int Cnt, int Size, int R, int L) { fa = Fa; val = Val; cnt = Cnt; size = Size; ch[0] = L; ch[1] = R; } } tree[N];
|
设计旋转
单旋(rotate)
首先考虑一下,我们要把一个点挪到根,那我们首先要知道怎么让一个点挪到它的父节点。单旋函数就是这个功能。
情况一
考虑下面的情况,我们要把节点3移动到它的父亲节点2之上(字母节点代表一颗子树):
这时候如果我们让X成为Y的父亲,只会影响到3个点的关系:
Y与3,3与2,3与1
根据二叉排序树的性质
Y会成为2的左儿子
2会成为3的右儿子
3会成为1的儿子(与原来2与1的情况相同)
经过变换之后,大概是这样:
情况二
那么反过来,我们把这个时候的节点2移到3节点之上,与第一个图是相同的,这就是第二种情况。
代码
首先我们写一个get函数,以获取此节点是其父亲的哪一个儿子:
1
| bool get(int x) { return x == tree[x].ch[1]; }
|
我们假设是从u->v,设计函数rotate如下:
1 2 3 4 5 6 7
| void rotate(int u, int v) { int u_fa = tree[u].fa; int v_fa = tree[v].fa; int son_u = get(u); int son_v = get(v); }
|
观察上图,我们发现节点3的Y子树的父亲更改了,可以发现下面两个规律:
- Y子树的位置与3在2中的位置相反
- Y子树在2的目标位置与3在2中的位置相同
那么我们先获取Y子树的位置,然后再把这几个要改变的点相互连接就好了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void rotate(int u) { int v = tree[u].fa; int v_fa = tree[v].fa; int son_u = get(u); int son_v = get(v); int change = tree[u].ch[son_u ^ 1]; tree[change].fa = v; tree[v].ch[son_u] = change; tree[v].fa = u; tree[u].ch[son_u ^ 1] = v; tree[u].fa = v_fa; tree[v_fa].ch[son_v] = u; }
|
双旋(splay)
splay(u,v)是实现把u节点直接搬到v节点。
最简单的办法,对于u这个节点,每次单旋直到v。
但是,众所周知出题老师拥有极强的卡时能力,可能会构造数据把单旋卡成n2,具体原因我也不清楚啦,反正不要这样用就好了,下面我们来写splay函数吧~
OI-Wiki把splay分成了六种情况啊!六种!太麻烦了,实际上就三种啦。
情况一
我们的目标节点v就是u的父亲,那么就直接rotate吧!
情况二
三个点连成了一条线……
这时候先把2旋转上去,再把3旋转上去就好了
1
| else if (get(u) == get(tree[u].fa)) rotate(tree[u].fa), rotate(x);
|
情况三
三个点歪七扭八,不知道是什么东西……
这时候把3旋转两次就好啦
代码
全部代码如下:
1 2 3 4 5 6 7 8 9 10
| void splay(int u, int v) { v = tree[v].fa; while (tree[u].fa != v) { if (tree[tree[u].fa].fa == v) rotate(u); else if (get(u) == get(tree[u].fa)) rotate(tree[u].fa), rotate(x); else rotate(u), rotate(u); } }
|
功能函数
Splay最伟大的两个函数写好了,我们就要实现他作为一颗BST树的功能了,一个一个来,我们慢慢讲,因为下面的内容可能还需要修改上面的两个旋转函数。
清空节点
1
| void clear(int u) { tree[u].ch[0] = tree[u].ch[1] = tree[u].fa = tree[u].fa = tree[u].size = tree[u].cnt = 0; }
|
维护Size
1
| void maintain(int u) { tree[u].size = tree[tree[u].ch[0]].size + tree[tree[u].ch[1]].size + tree[u].cnt; }
|
我们每一次旋转都会改变节点的Size值,因此要在rotate函数的最后添加这两句:
1 2
| maintain(u); maintain(v);
|
最后完成的rotate函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| void rotate(int u) { int v = tree[u].fa; int v_fa = tree[v].fa; int son_u = get(u); int son_v = get(v); int change = tree[u].ch[son_u ^ 1]; tree[change].fa = v; tree[v].ch[son_u] = change; tree[v].fa = u; tree[u].ch[son_u ^ 1] = v; tree[u].fa = v_fa; tree[v_fa].ch[son_v] = u; maintain(u); maintain(v); }
|
插入操作
插入操作是一个比较复杂的过程,具体步骤如下(假设插入的值为 k):
- 如果树空了,则直接插入根并退出。
- 如果当前节点的权值等于 k 则增加当前节点的大小并更新节点和父亲的信息,将当前节点进行 Splay 操作。
- 否则按照二叉查找树的性质向下找,找到空节点就插入即可(请不要忘记 Splay 操作)。
我们定义一个新变量root
,来代表根节点的位置。
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| void insert(int k) { int now = root; if (root == 0) { tree[++tot].fa = 0; tree[tot].val = k; tree[tot].cnt = tree[tot].size = 1; root = tot; } else { while (1) { tree[now].size++; if (tree[now].val == k) { tree[now].cnt++; splay(now, root); return; } int nxt = k < tree[now].val ? 0 : 1; if (!tree[now].ch[nxt]) { tree[++tot].fa = now; tree[tot].val = k; tree[tot].cnt = tree[tot].size = 1; tree[now].ch[nxt] = tot; splay(tot, root); return; } now = tree[now].ch[nxt]; } } }
|
同时还要注意每一次Splay是把当前节点移动到根,所以要在splay最后更新root
。
1 2 3 4 5 6 7 8 9 10 11
| void splay(int u, int v) { v = tree[v].fa; while (tree[u].fa != v) { if (tree[tree[u].fa].fa == v) rotate(u); else if (get(u) == get(tree[u].fa)) rotate(tree[u].fa), rotate(x); else rotate(u), rotate(u); root = u; } }
|
查询 k 的排名
根据二叉查找树的定义和性质,显然可以按照以下步骤查询 k 的排名:
- 如果 k 比当前节点的权值小,向其左子树查找。
- 如果 k 比当前节点的权值大,将答案加上左子树size和当前节点cnt的大小,向其右子树查找。
- 如果 k 与当前节点的权值相同,将答案加1并返回。
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| int rank(int k) { int ans = 0; int now = root; while (1) { if (k < tree[now].val) { now = tree[now].ch[0]; } else { ans += tree[tree[now].ch[0]].size; if (k == tree[now].val) { splay(now, root); return ans + 1; } ans += tree[now].cnt; now = tree[now].ch[1]; } } }
|
查询排名 k 的数
设 k 为剩余排名,具体步骤如下:
- 如果左子树非空且剩余排名 k 不大于左子树的大小 size,那么向左子树查找。
- 否则将 k 减去左子树的和根的大小。如果此时 k 的值小于等于 0,则返回根节点的权值,否则继续向右子树查找。
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| int kth(int k) { int now = root; while (1) { if (tree[now].ch[0] && k <= tree[tree[now].ch[0]].size) { now = tree[now].ch[0]; } else { k -= tree[now].cnt + tree[tree[now].ch[0]].size; if (k <= 0) { splay(now, root); return tree[now].val; } now = tree[now].ch[1]; } } }
|
查询前驱
前驱定义为小于 x 的最大的数,那么查询前驱可以转化为:将 x 插入(此时 x 已经在根的位置了),前驱即为 x 的左子树中最右边的节点,最后将 x 删除即可。
实现代码如下:
1 2 3 4 5 6 7 8
| int pre() { int now = tree[root].ch[0]; if (!now) return now; while (tree[now].ch[1]) now = tree[now].ch[1]; splay(now, root); return now; }
|
查询后继
后继定义为大于 x 的最小的数,查询方法和前驱类似:x 的右子树中最左边的节点。
实现代码如下:
1 2 3 4 5 6 7 8
| int nxt() { int now = tree[root].ch[1]; if (!now) return now; while (tree[now].ch[0]) now = tree[now].ch[0]; splay(now, root); return now; }
|
删除操作
删除操作也是一个比较复杂的操作,具体步骤如下:
首先将 x 旋转到根的位置。
- 如果 (有不止一个 x),那么将 tree[x].cnt 减 1 并退出。
- 否则,合并它的左右两棵子树即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| void del(int k) { rank(k); if (tree[root].cnt > 1) { tree[root].cnt--; maintain(root); return; } if (!tree[root].ch[0] && !tree[root].ch[1]) { clear(root); root = 0; return; } if (!tree[root].ch[0]) { int now = root; root = tree[root].ch[1]; tree[root].fa = 0; clear(now); return; } if (!tree[root].ch[1]) { int now = root; root = tree[root].ch[0]; tree[root].fa = 0; clear(now); return; } int now = root, x = pre(); tree[tree[now].ch[1]].fa = x; tree[x].ch[1] = tree[now].ch[1]; clear(now); maintain(root); }
|
最终Splay模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
| #include <iostream> #include <cstring> #include <cstdio> #define N 100005
using namespace std;
struct Node { int fa; int val; int cnt; int size; int ch[2]; Node() { fa = 0; val = 0; cnt = 0; size = 0; ch[0] = ch[1] = 0; } Node(int Fa, int Val, int Cnt, int Size, int R, int L) { fa = Fa; val = Val; cnt = Cnt; size = Size; ch[0] = L; ch[1] = R; } } tree[N]; int tot; int root;
bool get(int u) { return u == tree[u].ch[1]; }
void maintain(int u) { tree[u].size = tree[tree[u].ch[0]].size + tree[tree[u].ch[1]].size + tree[u].cnt; }
void clear(int u) { tree[u].ch[0] = tree[u].ch[1] = tree[u].fa = tree[u].fa = tree[u].size = tree[u].cnt = 0; }
void rotate(int u) { int v = tree[u].fa; int v_fa = tree[v].fa; int son_u = get(u); int son_v = get(v); int change = tree[u].ch[son_u ^ 1]; tree[change].fa = v; tree[v].ch[son_u] = change; tree[v].fa = u; tree[u].ch[son_u ^ 1] = v; tree[u].fa = v_fa; tree[v_fa].ch[son_v] = u; maintain(u); maintain(v); }
void splay(int u, int v) { v = tree[v].fa; while (tree[u].fa != v) { if (tree[tree[u].fa].fa == v) rotate(u); else if (get(u) == get(tree[u].fa)) rotate(tree[u].fa), rotate(x); else rotate(u), rotate(u); root = u; } }
void insert(int k) { int now = root; if (root == 0) { tree[++tot].fa = 0; tree[tot].val = k; tree[tot].cnt = tree[tot].size = 1; root = tot; } else { while (1) { tree[now].size++; if (tree[now].val == k) { tree[now].cnt++; splay(now, root); return; } int nxt = k < tree[now].val ? 0 : 1; if (!tree[now].ch[nxt]) { tree[++tot].fa = now; tree[tot].val = k; tree[tot].cnt = tree[tot].size = 1; tree[now].ch[nxt] = tot; splay(tot, root); return; } now = tree[now].ch[nxt]; } } }
int rank(int k) { int ans = 0; int now = root; while (1) { if (k < tree[now].val) { now = tree[now].ch[0]; } else { ans += tree[tree[now].ch[0]].size; if (k == tree[now].val) { splay(now, root); return ans + 1; } ans += tree[now].cnt; now = tree[now].ch[1]; } } }
int kth(int k) { int now = root; while (1) { if (tree[now].ch[0] && k <= tree[tree[now].ch[0]].size) { now = tree[now].ch[0]; } else { k -= tree[now].cnt + tree[tree[now].ch[0]].size; if (k <= 0) { splay(now, root); return tree[now].val; } now = tree[now].ch[1]; } } }
int pre() { int now = tree[root].ch[0]; if (!now) return now; while (tree[now].ch[1]) now = tree[now].ch[1]; splay(now, root); return now; }
int nxt() { int now = tree[root].ch[1]; if (!now) return now; while (tree[now].ch[0]) now = tree[now].ch[0]; splay(now, root); return now; }
void del(int k) { rank(k); if (tree[root].cnt > 1) { tree[root].cnt--; maintain(root); return; } if (!tree[root].ch[0] && !tree[root].ch[1]) { clear(root); root = 0; return; } if (!tree[root].ch[0]) { int now = root; root = tree[root].ch[1]; tree[root].fa = 0; clear(now); return; } if (!tree[root].ch[1]) { int now = root; root = tree[root].ch[0]; tree[root].fa = 0; clear(now); return; } int now = root, x = pre(); tree[tree[now].ch[1]].fa = x; tree[x].ch[1] = tree[now].ch[1]; clear(now); maintain(root); }
int main() { return 0; }
|