什么是Splay Tree
Splay Tree 就是一颗的二叉排序树,首先是二叉树,其次对于任意节点,其左子节点的值 ≤ \le ≤该节点的值 ≤ \le ≤右子节点的值,那与普通的二叉排序树有什么区别呢?它能使得树变得更加平衡,但是不像平衡树那样严格限制树的形状,使得左右子树高度差小于1。
- 基本思想:假设数据具有时间局部性,即一个数据访问后不久还会再次访问,Splay Tree 能让数据在访问后不久再次访问的时间变短
- 操作:Splay 伸展操作(将节点经过一系列旋转提升到根节点),包括左旋(zag)和右旋(zig)
平摊的复杂度为 O ( l o g N ) O(logN) O(logN)。
下面是一些声明,采用数组形式存放节点,val 存放原数组的值,lson 存放左子树编号,rson 存放右子树编号, p 存放父节点编号,sz 为当前节点个数,给节点编号,rt 记录了根节点的编号。
int val[MAX], //val[i]表示编号为i的节点的值
lson[MAX], //左儿子编号,没有则为0
rson[MAX], //右儿子编号,没有则为0
p[MAX], //父节点编号,没有则为0
sz = 0; //节点编号从1到sz,每次新增一个节点sz++
class Splay{
private:
int rt = 0; //根节点编号
Zig和Zag(单旋)操作
先看看简单的情况,假设当前节点为 x,现在想把它提升一个高度,就要和父节点 y 做旋转,如果 x 和 y 之间的边在 x 节点的右侧,则进行右旋,以 x 为轴,将右边的边往下旋转,左旋同理,如下图所示。
理解了什么是左旋右旋后,再来看看复杂一点的情况,拿右旋 x 为例,假设 x 有右儿子为 b ,首先将 b 放在 y 的左节点,再将 y 子树放在 x 的右节点上,如果原来的 y 有父节点 z,则将 x 的父节点设置为 z,左旋类似,旋转前后不改变遍历顺序,可以验证右旋前后先序遍历都是 c x b y a 。
按照上面的旋转方法,可以编写出单旋的代码
void Splay::zag(int x){ //左旋x
int y = p[x], z = p[y];
int b = lson[x];
rson[y] = b; //x有左节点b,将y的右节点设置成b
if(b) p[b] = y; //如果x有左节点b
lson[x] = y;
p[y] = x;
p[x] = z;
if(z){ //如果y不是根节点,存在父节点z
if(lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
void Splay::zig(int x){ //右旋x
int y = p[x], z = p[y];
int b = rson[x];
lson[y] = b; //x有右节点b,将y的左节点设置成b
if(b) p[b] = y;
rson[x] = y;
p[y] = x;
p[x] = z;
if(z){
if(lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
双旋操作
双旋就是进行两次单旋操作,将节点提升两个高度,具体而言,有之字形和一字形,共四类情况,而对应的旋转操作如下图所示:
对于一字形,如下第一张图,首先对 y 进行左旋,得到中间的树型,再对 x 进行左旋,最终将 x 提升了两个高度,另一种情况就不再展示。对于下图之字形情况,首先对 x 进行右旋,提升到中间节点,再对 x 进行左旋,x 提升到了最上面。
编写代码如下:
if(lson[z] == y && lson[y] == x){ // z
zig(y); // y 1.右旋
zig(x); // x 2.右旋
}
else if(rson[z] == y && rson[y] == x){ // z
zag(y); // y 1.左旋
zag(x); // x 2.左旋
}
else if(lson[z] == y && rson[y] == x){ // z
zag(x); // y
zig(x); // x 1.左旋,2.右旋
}
else{ // z
zig(x); // y
zag(x); // x 1.右旋,2.左旋
}
Splay操作
知道了单旋和双旋,那么如何进行Splay操作,即如何把节点提升至根节点呢?我们有以下算法:
(1)判断当前节点是否是根节点
Y:结束
N:(2) 判断父节点是否是根节点
Y:单旋,结束
N:双旋,并回到(1)
也就是说将 x 提升到根节点只需要一直做双旋,如果父节点是根节点的话,就只需要做一次单旋即可,直到当前节点是根节点为止,Splay操作完成,代码如下,时间复杂度为 O ( l o g N ) O(log N) O(logN)
void Splay::splay(int x){
while(p[x]){ //当x的父节点不为根节点,重复执行循环
int y = p[x], z = p[y];
if(!z){ //如果父亲节点是根节点,即p[y] = 0
if(lson[y] == x) zig(x); //如果x是y的左儿子,进行右旋
else zag(x); //否则进行左旋
}
else{
if(lson[z] == y && lson[y] == x){ // z
zig(y); // y 1.右旋
zig(x); // x 2.右旋
}
else if(rson[z] == y && rson[y] == x){ // z
zag(y); // y 1.左旋
zag(x); // x 2.左旋
}
else if(lson[z] == y && rson[y] == x){ // z
zag(x); // y
zig(x); // x 1.左旋,2.右旋
}
else{ // z
zig(x); // y
zag(x); // x 1.右旋,2.左旋
}
}
}
rt = x; //更新根结点
}
插入元素
插入元素只需要从根节点往下走,找到合适的位置满足二叉排序树的要求即可,具体而言,如果插入元素比当前节点小,则去左子树,如果大,则去右子树,下面是插入元素4的例子。
插入元素的时间复杂度为
O
(
l
o
g
N
)
O(log N)
O(logN),插入完新的元素后,还要将其提升至根节点,做一次 Splay 操作,代码如下:
void Splay::add(int v){
val[++sz] = v; //插入新的元素到val数组后
if(!rt) rt = sz; //设置根节点
else{
int x = rt;
while(true){
if(v < val[x]){ //去往左子树
if(lson[x]) x = lson[x];
else{ //找到插入位置了
lson[x] = sz;
p[sz] = x;
break;
}
}
else{ //去往右子树
if(rson[x]) x = rson[x];
else{
rson[x] = sz;
p[sz] = x;
break;
}
}
}
splay(sz); //将新插入的节点sz旋转到根节点
}
}
查询操作
查询值为 v 的节点的下标值,和插入元素思想类似,遇到小的到左边,遇到大的到右边,直到找到相等的或者到叶子节点为止,如果没有找到,则返回 0,否则返回值为 v 的元素下标,最后也要进行一次Splay操作,时间复杂度也为 O ( l o g N ) O(logN) O(logN)。
int Splay::find(int v){
if(!rt) return 0;
int x = rt;
while(x){
if(val[x] == v) break;
else if(v < val[x]) x = lson[x];
else x = rson[x];
}
if(x){ //如果搜索到了,则x为对应值为v的节点下标
splay(x); //将查询得到的x旋转到根节点
return x;
}
return 0;
}
查询最大最小值
寻找最大值只需一直遍历右子树,直到遍历到叶子节点为止,这个就是最大值,再进行一次Splay操作,将最大值提升到根节点,如下图所示,Splay 操作完后,根节点是没有右节点的。
时间复杂度也为
O
(
l
o
g
N
)
O(logN)
O(logN),代码如下:
int Splay::find_max(){
if(!rt) return 0;
int x = rt;
while(rson[x]) x = rson[x];
splay(x); //将x提升到根节点
return x;
}
int Splay::find_min() {
if(!rt) return 0;
int x = rt;
while(lson[x]) x = lson[x];
splay(x);
return x;
}
删除操作
删除值为 v 的节点,首先调用 find 函数,如果没有值为 v 的节点,find 函数则返回0,删除函数啥也不做,如果存在值为 v 的节点,则 find 函数已经将其 Splay 到了根节点,现在只要分情况讨论,删除根节点即可,共有4种情况,如果没有左右子树,则设置 rt 为 0 ,其他三种情况如下图所示。
void Splay::del(int v){ //用左子树的最大值代替根节点
int x = find(v);
if(!x) return; //如果没有该节点,则啥事不做
if(!lson[x] && !rson[x]) rt = 0; //因为有Splay操作,所以x现在为根节点,如果左右节点都没有,则设置根节点为0
else if(!lson[x]){ //没有左子树,则删除根节点后根节点设置为原来根节点的右子树
rt = rson[rt];
p[rt] = 0;
}
else if(!rson[x]){
rt = lson[rt];
p[rt] = 0;
}
else{ //左右子树均不为空,则从左子树中找到最大值,然后将rson[rt]变成这个节点的右儿子
Splay tree1, tree2;
tree1.rt = lson[rt];// rt
p[tree1.rt] = 0; // / \
// tree1 tree2
tree2.rt = rson[rt];
p[tree2.rt] = 0;
tree1.find_max();
rt = tree1.rt;
rson[rt] = tree2.rt;
p[tree2.rt] = rt;
}
}
上代码
#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
#define MAX 50050
int val[MAX], //val[i]表示编号为i的节点的值
lson[MAX], //左儿子编号,没有则为0
rson[MAX], //右儿子编号,没有则为0
p[MAX], //父节点编号,没有则为0
sz = 0; //节点编号从1到sz,每次新增一个节点sz++
class Splay{
private:
int rt = 0; //根节点编号
public:
void zag(int x); //对x进行左旋
void zig(int x); //对x进行右旋
void splay(int x); //将x变成根节点
void add(int v); //插入一个值为v的节点
int find(int v); //返回权值为v的节点的编号,没有则返回0
void del(int v); //删除权值为v的节点,如果没有则什么也不做
int find_max(); //返回最大值对应节点的编号
int find_min(); //返回最小值对应节点的编号
};
void Splay::zag(int x){ //左旋
int y = p[x], z = p[y];
int b = lson[x];
rson[y] = b; //x有左节点b,将y的右节点设置成b
if(b) p[b] = y; //如果x有左节点
lson[x] = y;
p[y] = x;
p[x] = z;
if(z){ //如果y不是根节点,存在父节点z
if(lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
void Splay::zig(int x){ //右旋
int y = p[x], z = p[y];
int b = rson[x];
lson[y] = b; //x有右节点b,将y的左节点设置成b
if(b) p[b] = y;
rson[x] = y;
p[y] = x;
p[x] = z;
if(z){
if(lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
void Splay::splay(int x){
while(p[x]){ //当x的父节点不为根节点,重复执行循环
int y = p[x], z = p[y];
if(!z){ //如果父亲节点是根节点,即p[y] = 0
if(lson[y] == x) zig(x); //如果x是y的左儿子,进行右旋
else zag(x); //否则进行左旋
}
else{
if(lson[z] == y && lson[y] == x){ // z
zig(y); // y 1.右旋
zig(x); // x 2.右旋
}
else if(rson[z] == y && rson[y] == x){ // z
zag(y); // y 1.左旋
zag(x); // x 2.左旋
}
else if(lson[z] == y && rson[y] == x){ // z
zag(x); // y
zig(x); // x 1.左旋,2.右旋
}
else{ // z
zig(x); // y
zag(x); // x 1.右旋,2.左旋
}
}
}
rt = x; //更新根结点
}
void Splay::add(int v){
val[++sz] = v;
if(!rt) rt = sz; //设置根节点
else{
int x = rt;
while(true){
if(v < val[x]){ //去往左子树
if(lson[x]) x = lson[x];
else{ //找到插入位置了
lson[x] = sz;
p[sz] = x;
break;
}
}
else{ //去往右子树
if(rson[x]) x = rson[x];
else{
rson[x] = sz;
p[sz] = x;
break;
}
}
}
splay(sz); //将新插入的节点sz旋转到根节点
}
}
int Splay::find(int v){
if(!rt) return 0;
int x = rt;
while(x){
if(val[x] == v) break;
else if(v < val[x]) x = lson[x];
else x = rson[x];
}
if(x){ //如果搜索到了,则x为对应值为v的节点下标
splay(x); //将查询得到的x旋转到根节点
return x;
}
return 0;
}
void Splay::del(int v){ //用左子树的最大值代替根节点
int x = find(v);
if(!x) return; //如果没有该节点,则啥事不做
if(!lson[x] && !rson[x]) rt = 0; //因为有Splay操作,所以x现在为根节点,如果左右节点都没有,则设置根节点为0
else if(!lson[x]){ //没有左子树,则删除根节点后根节点设置为原来根节点的右子树
rt = rson[rt];
p[rt] = 0;
}
else if(!rson[x]){
rt = lson[rt];
p[rt] = 0;
}
else{ //左右子树均不为空,则从左子树中找到最大值,然后将rson[rt]变成这个节点的右儿子
Splay tree1, tree2;
tree1.rt = lson[rt];// rt
p[tree1.rt] = 0; // / \
// tree1 tree2
tree2.rt = rson[rt];
p[tree2.rt] = 0;
tree1.find_max();
rt = tree1.rt;
rson[rt] = tree2.rt;
p[tree2.rt] = rt;
}
}
int Splay::find_max(){
if(!rt) return 0;
int x = rt;
while(rson[x]) x = rson[x];
splay(x); //将x提升到根节点
return x;
}
int Splay::find_min() {
if(!rt) return 0;
int x = rt;
while(lson[x]) x = lson[x];
splay(x);
return x;
}