各类平衡树的学习(零)——二叉搜索树BST

一.一道模板题.
题目:BZOJ3224.
题目大意:在序列上维护一个数据结构支持:
1.插入数 x x x.
2.删除数 x x x(只删一个).
3.查找 x x x的最小排名.
4.查找排名为 x x x的数.
5.查找 x x x的前驱.
6.查找 x x x的后继.
1 ≤ n ≤ 1 0 5 1\leq n\leq 10^5 1n105.

这种题目一般都会用平衡树这种数据结构来维护,而所有平衡树都得基于二叉搜索树BST,所以这篇文章会简单介绍一下这种神奇数据结构.


二.二叉搜索树的性质.

键值:二叉搜索树是一棵带点权二叉树,其中每个节点的点权被称为键值,方便起见,我们先假设BST上每一个节点的点权各不相同.

BST性质:对于二叉搜索树上的每一个节点,它的左儿子点权小于它的点权且右儿子点权大于它的点权,我们将这个性质称之为BST性质.

通过这一条性质可以看出,BST的中序遍历是一个升序序列.

BST性质是BST的核心,接下来所有在BST上的操作都要用到这个性质.


三.BST基本架构与建树.

我们这里用一个结构体来表示树上的一个节点,并用数组当内存池存结构体.

首先BST是一棵二叉树,我们需要存储每个节点的两个儿子 s [ 0 / 1 ] s[0/1] s[0/1].

又因为BST是一棵带权二叉树,所以我们记一下每个节点的键值 x x x.

最后为了保证BST中没有两个点键值相同,我们可以给每一个节点记 c n t cnt cnt表示这个节点的键值出现了几次;而为了下面我们要维护的一些操作还需要记 s i z siz siz表示子树 c n t cnt cnt和.

除此之外我们还需要记一下这棵树的节点个数 c n cn cn用来开内存池以及树根 r o t rot rot.

也就是说一个BST大概需要存这些东西:

struct tree{
  int x,s[2],siz,cnt;
}tr[N+9];
int cn,rot;

初始一棵空的BST可以直接没有任何节点,但是由于BST可能会用到查前驱后继时会遇到十分复杂的边界判定,于是我们建树的时候直接键入两个键值分别为无穷大和无穷小的点来避免边界判定.

建树代码如下:

void Pushup(int k){tr[k].siz=tr[tr[k].s[0]].siz+tr[tr[k].s[1]].siz+tr[k].cnt;}

int New_node(int x){
  tr[++cn]=tree();
  tr[cn].x=x;tr[cn].siz=tr[cn].cnt=1;
  return cn;
}

void Build(){cn=0;tr[rot=New_node(-INF)].s[1]=New_node(INF);Pushup(1);}



四.查询一个权值所在节点编号Find_val.

我们用 F i n d _ v a l ( x ) Find\_val(x) Find_val(x)表示查找一个键值 x x x所在的节点编号.

这个操作的实现可以通过BST的性质,从根往下找,如果 x x x大于当前节点的键值往右儿子找,小于往左儿子找,等于直接返回.

代码实现如下:

int Find_val(int x){
  int k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x]);
  return k?k:-1;
}



五.插入节点Insert.

我们用 I n s e r t ( x ) Insert(x) Insert(x)表示插入一个键值为 x x x的节点.

实现这个操作很简单,我们只要查找这个键值,如果最后查找到了这个键值的编号为 k k k,就直接给 t r [ k ] . c n t tr[k].cnt tr[k].cnt 1 1 1;否则我们把最后遍历到的节点的编号记为 k k k,新建一个键值为 x x x父亲为 k k k的点即可.

最后不要忘记把新建节点的所有祖先的信息更新.

代码如下:

void Insert(int x,int &k=rot){
  if (!k) {k=New_node(x);return;}
  if (tr[k].x==x) {++tr[k].cnt;Pushup(k);return;}
  Insert(x,tr[k].s[x>tr[k].x]);
  Pushup(k);
}



六.查询x的排名Query_rank.

我们把查询一个键值 x x x排名的函数称之为 Q u e r y _ r a n k ( x ) Query\_rank(x) Query_rank(x).

这个函数的实现很容易,我们只需要在查找这个键值的过程中,每次往右儿子走的时候就把左儿子的 s i z siz siz和当前点的 c n t cnt cnt加入答案中,最后若查找到了这个键值则再把这个点的左儿子的 s i z siz siz加入到答案中即可.

由于我们开始的时候多插入了一个不存在的键值无穷小节点,所以应该在原来排名的基础上减 1 1 1,但是排名本身就应该多加 1 1 1,所以互相抵消.

代码如下:

int Query_rank(int x){
  int res=0,k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x])
    if (x>tr[k].x) res+=tr[k].cnt+tr[tr[k].s[0]].siz;
  if (k) res+=tr[tr[k].s[0]].siz;
  return res;
}



七.查询排名为x的节点编号Find_rank.

我们将查询某个排名 x x x节点编号的操作称之为 F i n d _ r a n k ( x ) Find\_rank(x) Find_rank(x).

这个问题也很好解决.类似于在权值线段树上二分,我们从根开始往下,设当前点为 k k k,若 x ≤ t r [ t r [ k ] . l s ] . s i z x\leq tr[tr[k].ls].siz xtr[tr[k].ls].siz则往左儿子走;若 x > t r [ t r [ k ] . l s ] . s i z + t r [ k ] . c n t x>tr[tr[k].ls].siz+tr[k].cnt x>tr[tr[k].ls].siz+tr[k].cnt则往右儿子走并且让 x x x减去 t r [ t r [ k ] . l s ] + t r [ k ] . c n t tr[tr[k].ls]+tr[k].cnt tr[tr[k].ls]+tr[k].cnt;否则说明当前点 k k k是排名为 x x x的节点,直接返回.

同样的,由于我们已经存了一个不存在的无穷小节点,所以应该查询排名为 x + 1 x+1 x+1的节点.

代码如下:

int Find_rank(int p){
  ++p; 
  int k=rot;
  while (2333)
    if (p>tr[tr[k].s[0]].siz+tr[k].cnt) p-=tr[tr[k].s[0]].siz+tr[k].cnt,k=tr[k].s[1];
    else if (p<=tr[tr[k].s[0]].siz) k=tr[k].s[0];
      else return k;
}



八.查询前驱Lower和后继Upper.

我们将查询键值 x x x前驱节点编号的操作称之为 L o w e r ( x ) Lower(x) Lower(x),同理查询后继称之为 U p p e r ( x ) Upper(x) Upper(x).

以前驱为例,我们从根节点开始查找点权 x x x,在查找的过程中经过的所有节点都是备选答案;若最后查找到了 x x x,还需要在子树中查找,具体实现就是先到这个点的左儿子,然后不停跳右儿子直到不能再跳,这个时候到达的节点就是答案.

后继同理.

代码如下:

int Lower(int x){
  int res=1,k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x])
    if (tr[k].x<x&&tr[k].x>tr[res].x) res=k;
  for (k=tr[k].s[0];k;k=tr[k].s[1])
    if (tr[k].x<x&&tr[k].x>tr[res].x) res=k;
  return res;
}

int Upper(int x){
  int res=2,k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x])
    if (tr[k].x>x&&tr[k].x<tr[res].x) res=k;
  for (k=tr[k].s[1];k;k=tr[k].s[0])
    if (tr[k].x>x&&tr[k].x<tr[res].x) res=k;
  return res;
}



九.删除操作Erase.

我们把删除一个键值 x x x的操作称之为 E r a s e ( x ) Erase(x) Erase(x).

首先我们查找到这个点权 x x x所在的节点 k k k,然后 t r [ k ] . c n t tr[k].cnt tr[k].cnt 1 1 1,若 t r [ k ] . c n t tr[k].cnt tr[k].cnt不等于 0 0 0操作就结束了.

否则我们就需要真的把这个点删除(当然你一定要像替罪羊树那样不删好像也没什么锅…),接下来我们开始分类讨论:
1.如果这个节点没有儿子,直接让它消失.
2.如果这个节点只有其中一个儿子,直接让它代替它的父亲.
3.否则的话,我们找到当前这个节点子树中它的前驱或者后继,把它前驱或后继的信息拷贝到它上面,并把它的那个前驱或后继完全删除(即 c n t = 0 cnt=0 cnt=0)即可.容易发现这个时候它的那个前驱或者后继不会有两个儿子.

代码如下:

void Erase(int x,int v=1,int &k=rot){
  if (!k) return;
  if (x^tr[k].x) {Erase(x,v,tr[k].s[x>tr[k].x]);Pushup(k);return;}
  tr[k].cnt-=v;
  if (tr[k].cnt<0) tr[k].cnt;
  if (tr[k].cnt) {Pushup(k);return;}
  int ls=tr[k].s[0],rs=tr[k].s[1];
  if (!ls&&!rs) k=0;
  else if (!ls) k=rs;
    else if (!rs) k=ls;
      else{
        int nxt=rs;
        for (;tr[nxt].s[0];nxt=tr[nxt].s[0]);
        tr[k].cnt=tr[nxt].cnt;
        Erase(tr[nxt].x,tr[nxt].cnt);
        tr[k].x=tr[nxt].x;
      }
  Pushup(k);
}



十.时间复杂度分析.

容易发现这样子构造二叉查找树的深度期望是 O ( log ⁡ n ) O(\log n) O(logn)的,只要不特地去卡,期望一个操作的复杂度是 O ( log ⁡ n ) O(\log n) O(logn)的.

但是我们只需要插入一个有序序列就可使这棵树的深度变成 O ( n ) O(n) O(n)的,这也是为什么我们需要有平衡树这么一个东西的原因——平衡树的实现原理就是让这棵树的深度更加优秀.


十一.完整代码.

普通平衡树代码如下:

#include<bits/stdc++.h>
  using namespace std;

#define Abigail inline void
typedef long long LL;

const int N=100000,INF=(1<<31)-1;

struct tree{
  int x,fa,s[2],siz,cnt;
}tr[N+9];
int cn,rot;

void Pushup(int k){tr[k].siz=tr[tr[k].s[0]].siz+tr[tr[k].s[1]].siz+tr[k].cnt;}

int New_node(int x){
  tr[++cn]=tree();
  tr[cn].x=x;tr[cn].siz=tr[cn].cnt=1;
  return cn;
}

void Build(){cn=0;tr[rot=New_node(-INF)].s[1]=New_node(INF);Pushup(1);}

int Find_val(int x){
  int k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x]);
  return k?k:-1;
}

void Insert(int x,int &k=rot){
  if (!k) {k=New_node(x);return;}
  if (tr[k].x==x) {++tr[k].cnt;Pushup(k);return;}
  Insert(x,tr[k].s[x>tr[k].x]);
  Pushup(k);
}

int Query_rank(int x){
  int res=0,k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x])
    if (x>tr[k].x) res+=tr[k].cnt+tr[tr[k].s[0]].siz;
  if (k) res+=tr[tr[k].s[0]].siz;
  return res;
}

int Find_rank(int p){
  ++p; 
  int k=rot;
  while (2333)
    if (p>tr[tr[k].s[0]].siz+tr[k].cnt) p-=tr[tr[k].s[0]].siz+tr[k].cnt,k=tr[k].s[1];
    else if (p<=tr[tr[k].s[0]].siz) k=tr[k].s[0];
      else return k;
}

int Lower(int x){
  int res=1,k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x])
    if (tr[k].x<x&&tr[k].x>tr[res].x) res=k;
  for (k=tr[k].s[0];k;k=tr[k].s[1])
    if (tr[k].x<x&&tr[k].x>tr[res].x) res=k;
  return res;
}

int Upper(int x){
  int res=2,k=rot;
  for (;k&&tr[k].x^x;k=tr[k].s[x>tr[k].x])
    if (tr[k].x>x&&tr[k].x<tr[res].x) res=k;
  for (k=tr[k].s[1];k;k=tr[k].s[0])
    if (tr[k].x>x&&tr[k].x<tr[res].x) res=k;
  return res;
}

void Erase(int x,int v=1,int &k=rot){
  if (!k) return;
  if (x^tr[k].x) {Erase(x,v,tr[k].s[x>tr[k].x]);Pushup(k);return;}
  tr[k].cnt-=v;
  if (tr[k].cnt<0) tr[k].cnt;
  if (tr[k].cnt) {Pushup(k);return;}
  int ls=tr[k].s[0],rs=tr[k].s[1];
  if (!ls&&!rs) k=0;
  else if (!ls) k=rs;
    else if (!rs) k=ls;
      else{
        int nxt=rs;
        for (;tr[nxt].s[0];nxt=tr[nxt].s[0]);
        tr[k].cnt=tr[nxt].cnt;
        Erase(tr[nxt].x,tr[nxt].cnt);
        tr[k].x=tr[nxt].x;
      }
  Pushup(k);
}

Abigail getans(){
  int opt,x,n;
  scanf("%d",&n);
  Build();
  while (n--){
  	scanf("%d%d",&opt,&x);
  	switch (opt){
  	  case 1:
  	  	Insert(x);
  	  	break;
  	  case 2:
  	  	Erase(x);
  	  	break;
  	  case 3:
  	  	printf("%d\n",Query_rank(x));
  	  	break;
  	  case 4:
  	  	printf("%d\n",tr[Find_rank(x)].x);
  	  	break;
  	  case 5:
  	  	printf("%d\n",tr[Lower(x)].x);
  	  	break;
  	  case 6:
  	  	printf("%d\n",tr[Upper(x)].x);
  	  	break;
  	}
  }
}

int main(){
  getans();
  return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值