一、二叉排序樹
1、定義
二叉排序樹(Binary Sort Tree),又稱二叉查詢樹(Binary Search Tree),亦稱二叉搜尋樹。
二叉排序樹或者是一棵空樹,或者是具有下列性質的二叉樹:
1、若左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
2、若右子樹不空,則右子樹上所有節點的值均大於它的根節點的值;
3、左、右子樹也分別為二叉排序樹。
下面的這幅圖就是一個二叉排序樹
2、二叉排序樹的查詢
二叉排序樹查詢在在最壞的情況下,需要的查詢時間取決於樹的深度:
1、當二叉排序樹接近於滿二叉樹時,其深度為\(log_2n\),因此最壞情況下的查詢時間為\(O(log_2n)\),與折半查詢是同數量級的。
2、但是當二叉樹如下圖所示形成單枝樹時,其深度為\(n\),最壞情況下查詢時間為\(O(n)\),與順序查詢屬於同一數量級。
所以,為了保證二叉排序樹的查詢有較高的查詢速度,希望該二叉樹接近於滿二叉樹
或者二叉樹的每一個節點的左、右子樹深度儘量相等
而\(Splay\)可以很好地解決這一問題
二、Splay
伸展樹(Splay Tree),也叫分裂樹,是一種二叉排序樹,它能在\(O(log n)\)內完成插入、查詢和刪除操作。它由丹尼爾·斯立特Daniel Sleator 和 羅伯特·恩卓·塔揚Robert Endre Tarjan 在1985年發明的。
1、結構體定義
struct trr{
int son,ch[2],fa,cnt,val;
}tr[maxn];
其中\(son\)為兒子數量
\(ch[0]\)為左兒子的編號,\(ch[1]\)為右兒子的編號
\(fa\)為當前節點的父親節點
\(cnt\)為當前節點的數量
\(val\)為當前節點的權值
2、旋轉操作
旋轉操作是\(Splay\)中的基本操作
每次有新節點加入、刪除或查詢時,我們都將其旋轉至根節點
這樣可以保持\(BST\)的平衡
我們拿實際的圖來演示一下
在這幅圖中,\(x\)是\(y\)的左兒子,而我們想要將\(x\)旋轉至\(y\)的位置
首先,根據\(BST\)的性質,\(x<y\)
因此旋轉後,\(y\)應該變為\(x\)的右兒子
那\(x\)原來的右兒子\(b\)呢
根據性質有\(x<b<y\),而\(y\)在旋轉後恰好沒有左兒子,因此我們讓\(b\)當\(y\)的左兒子
\(y\)的右兒子\(c\)和\(x\)的左兒子\(b\)保持不變即可
旋轉後的圖變成了下面這個樣子
旋轉後的圖仍滿足\(BST\)的性質
但實際上,我們只列舉出了\(4\)種情況中的一種
1、\(y\)是\(z\)的左兒子,\(x\)是\(y\)的左兒子
2、\(y\)是\(z\)的左兒子,\(x\)是\(y\)的右兒子
3、\(y\)是\(z\)的右兒子,\(x\)是\(y\)的右兒子
4、\(y\)是\(z\)的右兒子,\(x\)是\(y\)的左兒子
如果對於每一種情況我們都分別列舉一遍會很麻煩
根據\(yyb\)神犇的總結
1、\(x\)變到原來\(y\)的位置
2、\(y\)變成了 \(x\)原來在\(y\)的相對 的那個兒子
3、\(y\)的非\(x\)的兒子不變 \(x\)的 \(x\)原來在\(y\)的 那個兒子不變
4、\(x\)的 \(x\)原來在\(y\)的 相對的 那個兒子 變成了 \(y\)原來是 \(x\)的那個兒子
程式碼如下
void push_up(int x){
tr[x].son=tr[tr[x].ch[0]].son+tr[tr[x].ch[1]].son+tr[x].cnt;
//當前節點兒子數量等於左兒子數量加右兒子數量加當前節點數量
}
void xuanzh(int x){
int y=tr[x].fa;
int z=tr[y].fa;
int k=(tr[y].ch[1]==x);
//判斷x是否是y的右兒子
tr[z].ch[tr[z].ch[1]==y]=x;
tr[x].fa=z;//x變到原來y的位置
tr[y].ch[k]=tr[x].ch[k^1];
tr[tr[x].ch[k^1]].fa=y;
//x的原來在x在y的相對位置的那個兒子變成了y原來是x的那個兒子
tr[x].ch[k^1]=y;
tr[y].fa=x;
//y變成了x原來在y的相對的那個兒子
push_up(y);
push_up(x);
//更新節點資訊
}
3、將一個節點上旋至規定點
我們是不是對於某一個節點連續進行兩次旋轉操作就可以呢
一般情況下是可以的,但是如果遇到下面的情況就不可行了
我們要把\(4\)旋轉到\(1\)的位置
如果我們一直將\(4\)進行旋轉操作,那麼旋轉兩次後的圖變成了下面這樣
我們會發現\(1-3-5\)這一條鏈仍然存在
只不過是\(4\)號節點跑到了原來\(1\)號節點的位置
這樣的話,\(Spaly\)就失去了意義
因此,我們分情況討論:
(\(x\)是\(y\)的兒子節點,\(y\)是\(z\)的兒子節點,將\(x\)旋轉到\(z\))
1、\(x\)和\(y\)分別是\(y\)和\(z\)的同一個兒子
先旋轉\(y\)再旋轉\(x\)
2、\(x\)和\(y\)分別是\(y\)和\(z\)不同的兒子
將\(x\)旋轉兩次
程式碼
void splay(int x,int goal){
//將x旋轉至目標節點goal的兒子
while(tr[x].fa!=goal){
int y=tr[x].fa;
int z=tr[y].fa;
if(z!=goal){
(tr[y].ch[0]==x)^(tr[z].ch[0]==y)?xuanzh(x):xuanzh(y);
}
//分情況討論:同位置兒子旋轉y,不同位置兒子旋轉x
xuanzh(x);
//最後旋轉x
}
if(goal==0) rt=x;
//如果旋轉到根節點,將根節點更新為x
}
4、查詢操作
類似於二分查詢
從根節點開始,如果要查詢的值大於該點的值,向右兒子遞迴
否則向左兒子遞迴
如果當前位置的值已經是要查詢的數,則將該節點旋轉至根節點,方便之後的操作
void zhao(int x){
//查詢x的位置,並將其旋轉至根節點
int u=rt;
if(!u) return;//樹為空
while(tr[u].ch[x>tr[u].val] && x!=tr[u].val){
//當存在兒子並且當前位置的值不等於x
u=tr[u].ch[x>tr[u].val];//跳轉到兒子
}
splay(u,0);
//將當前位置旋轉到根節點
}
5、插入操作
和查詢操作類似,也是從根節點開始
如果要插入的值大於該點的值,向右兒子遞迴
否則向左兒子遞迴
如果可以在原樹中找到當前值,把節點的數量加一即可
否則再新建一個節點
void ad(int x){
//插入價值為x的節點
int u=rt,fa=0;
while(u && tr[u].val!=x){
fa=u;
u=tr[u].ch[x>tr[u].val];
//向兒子遞迴
}
if(u) tr[u].cnt++;
//如果當前節點已經存在,節點的個數加一
else {
//如果不存在,建立一個新的節點
u=++tot;
if(fa) tr[fa].ch[x>tr[fa].val]=u;
tr[tot].ch[1]=0;
tr[tot].ch[0]=0;
tr[tot].val=x;
tr[tot].fa=fa;
tr[tot].cnt=1;
tr[tot].son=1;
}
splay(u,0);//將當前節點上旋至根節點
}
6、查詢前驅和後繼
我們要查詢某一個數\(x\)的前驅和字尾
首先我們要使用查詢操作,將\(x\)節點旋轉到根節點
如果查詢前驅,那麼前驅就是左子樹中權值最大的節點
那我們就從左子樹開始,一直向右子樹跳,直到沒有右子樹為止
查詢後繼也是同樣
int qq_hj(int x,int jud){
//jud為0查詢前驅,為1查詢字尾
zhao(x);
//將x旋轉至根節點
int u=rt;
if((tr[u].val>x && jud) || (tr[u].val<x && !jud)){
return u;
}
//如果無法繼續向下跳,返回當前節點
u=tr[u].ch[jud];
while(tr[u].ch[jud^1]){
u=tr[u].ch[jud^1];
}
//否則繼續向下跳
return u;
}
7、刪除操作
如果我們要刪除某一個數\(x\)
那麼這一個數的權值一定介於它的前驅和它的後繼之間
所以我們可以先把它的前驅旋轉至根節點
然後把它的後繼旋轉到它的前驅作為前驅的右兒子
這時,前驅的左兒子恰好比前驅大、後繼小,正是我們想要刪除的值
void sc(int x){
int qq=qq_hj(x,0);
//求出前驅
int hj=qq_hj(x,1);
//求出後繼
splay(qq,0);
//將前驅旋轉至根節點
splay(hj,qq);
//將後繼旋轉至前驅的右兒子
int willsc=tr[hj].ch[0];
//找出要刪除的數
if(tr[willsc].cnt>1){
tr[willsc].cnt--;
splay(willsc,0);
} else {
tr[hj].ch[0]=0;
}
//刪除該節點
}
8、查詢第k小的值
從根節點開始,如果左子樹的兒子數大於\(k\),向左子樹查詢
否則向右子樹查詢
遞迴解決問題即可
int kth(int x){
int u=rt;
if(tr[u].son<x) return 0;
//如果樹的節點數小於x,查詢失敗
while(1){
int y=tr[u].ch[0];
if(x>tr[y].son+tr[u].cnt){
x-=(tr[y].son+tr[u].cnt);
u=tr[u].ch[1];
//向右子樹查詢
} else {
if(x<=tr[y].son) u=y;
else return tr[u].val;
//向左子樹查詢
}
}
}
練習題(洛谷P3369)
一道很基礎的板子題,直接附上程式碼
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
#define INF 0x3f3f3f3f
struct trr{
int son,ch[2],fa,cnt,val;
}tr[maxn];
int n,tot,rt;
void push_up(int x){
tr[x].son=tr[tr[x].ch[0]].son+tr[tr[x].ch[1]].son+tr[x].cnt;
}
void xuanzh(int x){
int y=tr[x].fa;
int z=tr[y].fa;
int k=(tr[y].ch[1]==x);
tr[z].ch[tr[z].ch[1]==y]=x;
tr[x].fa=z;
tr[y].ch[k]=tr[x].ch[k^1];
tr[tr[x].ch[k^1]].fa=y;
tr[x].ch[k^1]=y;
tr[y].fa=x;
push_up(y);
push_up(x);
}
void splay(int x,int goal){
while(tr[x].fa!=goal){
int y=tr[x].fa;
int z=tr[y].fa;
if(z!=goal){
(tr[y].ch[0]==x)^(tr[z].ch[0]==y)?xuanzh(x):xuanzh(y);
}
xuanzh(x);
}
if(goal==0) rt=x;
}
void zhao(int x){
int u=rt;
if(!u) return;
while(tr[u].ch[x>tr[u].val] && x!=tr[u].val){
u=tr[u].ch[x>tr[u].val];
}
splay(u,0);
}
void ad(int x){
int u=rt,fa=0;
while(u && tr[u].val!=x){
fa=u;
u=tr[u].ch[x>tr[u].val];
}
if(u) tr[u].cnt++;
else {
u=++tot;
if(fa) tr[fa].ch[x>tr[fa].val]=u;
tr[tot].ch[1]=0;
tr[tot].ch[0]=0;
tr[tot].val=x;
tr[tot].fa=fa;
tr[tot].cnt=1;
tr[tot].son=1;
}
splay(u,0);
}
int qq_hj(int x,int jud){
zhao(x);
int u=rt;
if((tr[u].val>x && jud) || (tr[u].val<x && !jud)){
return u;
}
u=tr[u].ch[jud];
while(tr[u].ch[jud^1]){
u=tr[u].ch[jud^1];
}
return u;
}
void sc(int x){
int qq=qq_hj(x,0);
int hj=qq_hj(x,1);
splay(qq,0);
splay(hj,qq);
int willsc=tr[hj].ch[0];
if(tr[willsc].cnt>1){
tr[willsc].cnt--;
splay(willsc,0);
} else {
tr[hj].ch[0]=0;
}
}
int kth(int x){
int u=rt;
if(tr[u].son<x) return 0;
while(1){
int y=tr[u].ch[0];
if(x>tr[y].son+tr[u].cnt){
x-=(tr[y].son+tr[u].cnt);
u=tr[u].ch[1];
} else {
if(x<=tr[y].son) u=y;
else return tr[u].val;
}
}
}
int main(){
int n;
scanf("%d",&n);
ad(INF);
ad(-INF);
for(int i=1;i<=n;i++){
int aa,bb;
scanf("%d%d",&aa,&bb);
if(aa==1) ad(bb);
else if(aa==2) sc(bb);
else if(aa==3) {
zhao(bb);
int ans=tr[tr[rt].ch[0]].son;
printf("%d\n",ans);
} else if(aa==4){
int ans=kth(bb+1);
printf("%d\n",ans);
} else if(aa==5){
int ans=qq_hj(bb,0);
printf("%d\n",tr[ans].val);
} else {
int ans=qq_hj(bb,1);
printf("%d\n",tr[ans].val);
}
}
return 0;
}