Splay學習筆記


一、二叉排序樹

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、查找操作

類似於二分查找

從根節點開始,如果要查詢的值大於該點的值,向右兒子遞歸

否則向左兒子遞歸

如果當前位置的值已經是要查找的數,則將該節點旋轉至根節點

所以答案就是此時根的左兒子的兒子數,注意如果根節點的值小於x,要加上根節點的數量

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<cstdio>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=1e6+5;
const int INF=0x3f3f3f3f;
struct trr{
	int son,ch[2],fa,cnt,val;
}tr[maxn];
int n,rt,tot;
void push_up(rg int da){
	tr[da].son=tr[tr[da].ch[0]].son+tr[tr[da].ch[1]].son+tr[da].cnt;
}
void xuanzh(rg int x){
	rg int y=tr[x].fa;
	rg int z=tr[y].fa;
	rg 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(rg int x,rg int goal){
	while(tr[x].fa!=goal){
		rg int y=tr[x].fa;
		rg 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(rg int x){
	rg int now=rt;
	if(!now) return;
	while(tr[now].ch[x>tr[now].val] && x!=tr[now].val){
		now=tr[now].ch[x>tr[now].val];
	}
	splay(now,0);
}
void ad(rg int x){
	rg int now=rt,fa=0;
	while(now && tr[now].val!=x){
		fa=now;
		now=tr[now].ch[x>tr[now].val];
	}
	if(now) tr[now].cnt++;
	else {
		now=++tot;
		if(fa) tr[fa].ch[x>tr[fa].val]=now;
		tr[tot].ch[1]=tr[tot].ch[0]=0;
		tr[tot].val=x;
		tr[tot].fa=fa;
		tr[tot].cnt=1;
		tr[tot].son=1;
	}
	splay(now,0);
}
int qq_hj(rg int x,rg int jud){
	zhao(x);
	rg int now=rt;
	if((tr[now].val>x && jud) || (tr[now].val<x && !jud)){
		return now;
	}
	now=tr[now].ch[jud];
	while(tr[now].ch[jud^1]) now=tr[now].ch[jud^1];
	return now;
}
void sc(rg int x){
	rg int qq=qq_hj(x,0),hj=qq_hj(x,1);
	splay(qq,0);
	splay(hj,qq);
	rg int now=tr[hj].ch[0];
	if(tr[now].cnt>1){
		tr[now].cnt--;
		splay(now,0);
	} else {
		tr[hj].ch[0]=0;
	}
}
int kth(rg int k){
	rg int now=rt;
	if(tr[now].son<k) return 0;
	while(1){
		rg int y=tr[now].ch[0];
		if(k>tr[y].son+tr[now].cnt){
			k-=(tr[y].son+tr[now].cnt);
			now=tr[now].ch[1];
		} else {
			if(k<=tr[y].son) now=y;
			else return tr[now].val;
		}
	}
}
int main(){
	n=read();
	ad(INF);
	ad(-INF);
	rg int aa,bb,ans;
	for(int i=1;i<=n;i++){
		aa=read(),bb=read();
		if(aa==1) ad(bb);
		else if(aa==2) sc(bb);
		else if(aa==3) {
			zhao(bb);
			ans=tr[tr[rt].ch[0]].son+(tr[rt].val<bb?tr[rt].cnt:0);
			printf("%d\n",ans);
		} else if(aa==4){
			ans=kth(bb+1);
			printf("%d\n",ans);
		} else if(aa==5){
			ans=qq_hj(bb,0);
			printf("%d\n",tr[ans].val);
		} else {
			ans=qq_hj(bb,1);
			printf("%d\n",tr[ans].val);
		}
	}
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM