二叉搜索树

-1.想看代码的人

luoguP3369 普通平衡树,想学的人往下翻

#include "bits/stdc++.h"
using namespace std;
int son[100010][2],value[100010],ran_dom[100010],size[100010],root,t;
void update(int p)
{
	size[p]=size[son[p][0]]+size[son[p][1]]+1;
}
void rotate(int &p,bool op){
	int a=son[p][!op];
	son[p][!op]=son[a][op];
	son[a][op]=p;
	update(p);
	update(a);
	p=a;
}
void insert(int &p,int v){
	if(!p){
		p=++t;
		value[p]=v;
		ran_dom[p]=rand();
		size[p]=1;
		return;
	}
	size[p]++;
	bool op=v>value[p];
	insert(son[p][op],v);
	if(ran_dom[son[p][op]]>ran_dom[p])rotate(p,!op);
}
void delet_(int &p,int v)
{
    if(!p)return;
	if(v==value[p]){
		if(son[p][0]&&son[p][1]){
			bool op=ran_dom[son[p][0]]>ran_dom[son[p][1]];
			rotate(p,op),delet_(son[p][op],v);
		}
		else{
			p=son[p][0]|son[p][1]; 
			return;
		}
	}
	else delet_(son[p][v>value[p]],v);
	update(p);
}
int rank(int p,int v){
	if(!p)return 1;
	if(v>value[p])return rank(son[p][1],v)+size[son[p][0]]+1;
	else return rank(son[p][0],v);
}
int knar(int p,int v){
	int op=size[son[p][0]]+1;
	if(v<op)return knar(son[p][0],v);
	else if(v>op)return knar(son[p][1],v-op);
	else return value[p];
}
int pre(int p,int v){
	if(!p)return -9999999;
	if(v>value[p])return max(pre(son[p][1],v),value[p]);
	else return pre(son[p][0],v);
}
int erp(int p,int v){
	if(!p)return 9999999;
	if(v<value[p])return min(erp(son[p][0],v),value[p]);
	else return erp(son[p][1],v);
}
int main(){
	int n,op,m;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>op>>m;
		if(op==1)insert(root,m);
		if(op==2)delet_(root,m);
		if(op==3)cout<<rank(root,m)<<endl;
		if(op==4)cout<<knar(root,m)<<endl;
		if(op==5)cout<<pre(root,m)<<endl;
		if(op==6)cout<<erp(root,m)<<endl;
	}
	return 0;
}

0.前置知识

构造一棵树。中序遍历。

1.引入

二叉搜索树是一种神秘的高级数据结构,他可以做很多神秘的高级操作,比如说 l o g n logn logn排序等等。它把一些数据存在了一棵树中,这棵树严格遵循着一种特定的构造规律。

2.构造规律

一般来说,二叉搜索树遵循这样一个规律:
对于树中任意一颗子树来说,它的根节点的左子树的所有节点都比根节点小,右子树的所有节点都比根节点大。根节点的左右子树也是二叉搜索树。
在这里插入图片描述
比如这样。
更直观的理解,如果你把一颗二叉搜索树标准的放置,从左到右看,它的点权应该是有序的。或者说,它的中序遍历是有序的。
每次插入一个点时,首先来到根节点的位置,如果他比这个节点小那就走到这个节点的左子节点,否则去右子节点,然后持续做这个操作,直到这个节点为空,把他放在这里。
代码:

void insert(int &p,int v){
	if(!p){
		p=++t;
		value[p]=v;
		return;
	}
	if(v>value[p])insert(r[p],v);
	else insert(l[p],v);
}

那么删除呢?
删除的时候,有三种可能:

  1. 删除叶子结点
    直接删除

  2. 删除有一个子节点的节点
    删除节点并将其子节点移动至该点位置。
    在这里插入图片描述
    在这里插入图片描述

  3. 删除有两个子节点的节点
    找到这个节点的右子树的中序遍历的第一个节点(其实就是找后继),把他和这个后继交换,然后删除。(或者找前驱应该也可行)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3.实现操作

刚才说到了,二叉搜索树中序遍历是有序的,那么我们就可以利用这点来排序。还可以查找某个数,找最大值,找最小值,求排名,还有找前驱,后继(大于x最小,小于x最大)
还有很多神秘操你们自己去探索吧。

4.隐秘问题

有时,这样一个好好地二叉搜索树可能会被毒瘤数据卡爆。
比如 1 2 3 4 5 6 7 8 。 98765234523454524321哈哈被你发现了哈哈哈哈哈哈哈 \color{green}1\color{blue}2\color{yellow}3\color{purple}4\color{red}5\color{pink}6\color{mint}7\color{gray}8\color{black}。\color{white}\text{98765234523454524321哈哈被你发现了哈哈哈哈哈哈哈} 1234567898765234523454524321哈哈被你发现了哈哈哈哈哈哈哈
这种毒瘤会让二叉搜索树在建立时退化成一条链。
在这里插入图片描述
所以

5.平衡二叉树

有好多种平衡二叉树,总的来说就是让这个二叉树期望深度成为 l o g n logn logn,不成为像上图那样的链。
先来讲第一种:
这是一种严格平衡的算法(即任意节点的两个子树的深度差<=1)
如果出现了不平衡的二叉树,就需要调整。
怎么调整呢?这里就要用到一种非常玄幻的操作:旋转。
在这种算法中,由于他是需要严格平衡的,所以一共由四种(不算对称的情况,有两种)情况:
第一种情况,是根的左子树深度大,左子树的左子树深度大。在下图中, α β δ \alpha\beta\delta αβδ都代表着子树,其中 β \beta β更深。很显然,为了平衡,需要把 β \beta β向上转。
在这里插入图片描述
这种情况下,我们把y提到根的位置,然后因为 α \alpha α比x小,放在x的左子节点的位置。x的右子节点保留是 δ \delta δ,y的左子节点保留是 β \beta β
变成这样:
在这里插入图片描述
这样 / b e t a /beta /beta就向上移动了一层,使得我们离平衡进了一步。这种旋转称作左左旋转。


如果反过来(也就是跟的右子树深度大,右子树的右子树深度大),也是这样旋转。这就是右右旋转。


第三种情况:根的左子树深,左子树的右子树深。
还是这个图,但是这次是 α \alpha α更深。
在这里插入图片描述
这次处理就需要添加一些细节。要旋转两次,第一次旋转是旋转把 / a l p g a /alpga /alpga旋转到左子节点,和右右旋转是一样的,旋转结果是这样:
在这里插入图片描述
第二次旋转转是把 α \alpha α转到根节点,这次是左左旋转,结果转成这样:
在这里插入图片描述
他不就平衡了吗。所以先右右,再左左,就转完了。这就是左右旋转。


反之就是右左旋转。


四种情况都说完了,对每个节点都判断一下就行了。
这种算法叫做AVL算法,他有四种旋转,写起来非常繁琐。我们一般不写他,但是他确实是严格的二叉平衡树。由于代码非常繁琐,这里就不贴了,想看代码的出门左转
为什么要这么繁琐,因为在旋转的同时要保证他二叉搜索树的特性。读者可以自己试一试,看看这些树的中序排列有没有变。


还有一种方便得多的算法。
这种方法首先给每个点赋一个随机权值 r r r,然后根据原来的点权 v v v和这个 r r r建立一颗确定的树。这棵树要求满足以 v v v为关键字是一颗二叉搜索树,以 r r r为关键字是一个堆,也就是tree+heap,这就是算法名字由来:Treap。
虽然说这棵树的形态是随机的,但是他的期望深度是 l o g n log n logn的。
为什么呢?我们来做个思想实验。
有这么一棵树,你不断随机往里加节点,节点在任意位置的概率是同等的,随着树深度的增加,截面也增加了,出现极端状况的概率也就减小了。如果要出现卡爆的极端情况,那就需要每层都出现极端情况,这个概率不就小到指数级爆炸抽搐了吗。
这种算法另一个好处是只用写两种(其实只用一种,另一种直接反过来)旋转。
什么旋转呢?其实和刚才AVL的左左和右右旋转是一样的。。
那我就直接放代码了

void rotate_l(int &p){
	int a=left[p];
	left[p]=right[a];
	right[a]=p;
	p=a;
}
void rotate_r(int &p){
	int a=right[p];
	right[p]=left[a];
	left[a]=p;
	p=a;
}

如果看得够仔细的话,可以发现,这个代码的赋值形成了一个环形回路。
在这里插入图片描述
这不就很好记了吗。
再给一个插入的完整代码:

void insert(int &p,int v){
	if(!p){
		p=++t;
		value[p]=v;
		ran_dom[p]=rand();
		return;
	}
	if(v>value[p]){
		insert(r[p],v);
		if(ran_dom[r[p]]<ran_dom[p]){
			int a=r[p];
			r[p]=l[a];
			l[a]=p;
			p=a;
		}
	}else{
		insert(l[p],v);
		if(ran_dom[l[p]]<ran_dom[p]){
			int a=l[p];
			l[p]=r[a];
			r[a]=p;
			p=a;
		}
	}
}

用这个代码就可以往里加各种东西,然后在短时间内完成高妙操作。
在这里插入图片描述
在这里插入图片描述加油吧!
再附一个及精妙的宝藏

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值