//https://img2018.cnblogs.com/blog/1646268/201908/1646268-20190806114008215-138720377.jpg

二叉搜索树

性质

二叉搜索树是一种具有以下性质的树:

  1. 如果左子树不为空的话,左子树的所有节点的值都小于根节点的值
  2. 如果右子树不为空的话,右子树的所有节点的值都大于根节点的值
  3. 他的左右子树也都是二叉搜索树

下面就是一棵二叉搜索树

image

操作

二叉搜索树在一般的情况下都可以较为高效的完成一些操作,但在特殊情况下会退化成链。

二叉搜索树中的一个点代表的值可以是多个相同值的点的一个整体,也就是说,每一个值可能有重复的,但二叉搜索树里面不会有重复的,如果重复只需要在计数器上加一即可,这样也可以省空间,

下面以平衡树的模板题为例

查找

从根节点开始找,如果当前点的值就是要查找的值,那么就返回当前点的编号。
如果当前点的值大于要找的值,就去左子树去找。
如果当前点的值小于要找的值,就去右子树去找。

代码实现的话很简单,注意别打错下标就好,因为题目里面没有这个操作所以不放代码了。

插入

当你想要插入某一个值 k 的时候,操作和查找是差不多的,因为你要插入的话要先找到 k 插入的位置,当有点的值和 k 相等时,可以直接当前值的点数加一,反之开一个新点,然后就就是根据 k 和当前点的大小来判断是往哪里插入。

代码实现

void add(int &x,int k)
{
	if(x==0)
	{
		x=++cnt;
		e[x].siz=e[x].cnt=1;
		e[x].val=k;
		return ;
	}
	if(e[x].val==k)
	{
		e[x].cnt++;
		e[x].siz++;
		return ;
	}
	if(e[x].val<k)
	{
		add(rs1,k);
		p_p(x);
		return ;
	}
	if(e[x].val>k)
	{
		add(ls1,k);
		p_p(x);
		return ;
	}
	return ;
}

删除

我们可以先找到当前节点。如果左子树为空,就把右子树提上来,如果右子树为空,就把左子树提上来。否则,删去右子树最小的节点(这个节点的左子树一定为空),把这个节点移动到当前节点的位置上来。

代码实现

int fid(int &x)
{
	if(e[x].ls)
	{
		int res=fid(ls1);
		p_p(x);
		return res;
	}
	int res=x;
	x=e[x].rs;
	return res;
}
void dele(int &x,int k)
{
	if(e[x].val==k)
	{
		if(e[x].cnt>1)
		{
			e[x].cnt--;
			e[x].siz--;
			return ;
		}
		else
		{
			if(e[x].ls==0)
			{
				x=rs1;
				return ;
			}
			if(e[x].rs==0)
			{
				x=ls1;
				return ;
			}
			else
			{
				int xx=fid(rs1);
				e[x].val=e[xx].val;
				e[x].cnt=e[xx].cnt;
				p_p(x);
				return ;
			}
		}
	}
	if(e[x].val<k)
	{
		dele(rs1,k);
		p_p(x);
		return ;
	}
	if(e[x].val>k)
	{
		dele(ls1,k);
		p_p(x);
		return ;
	}
	return ;
}

查询排名

首先如果要是 k 刚好是在当前点的排名上,也就是 k==e[x].val的话,我们可以直接返回当前值的排名,也就是 e[e[x].ls].siz+1
我们还是和之前一样,如果当前要查询的 k 的值是大于当前点的值,也就是 k>e[x].val 的话,我们就要从当前点的右子树里面找了,但是在返回当前值的排名的时候,我们是要加上当前点的左子树大小和当前点的 cnt 值,因为如果你是去右子树里面找的排名的话得到的是 k 在当前点右子树里的排名,所以要加上,而当 k<e[x].val 的时候,我们可以想到是不用加的,因为 k 在当前点左子树里的排名就是他在整棵树里的排名。

代码实现

inline int getrk(int x,int k)
{
//	cout<<"FUCK"<<endl;
	if(e[x].val==k)return e[ls1].siz+1;
	if(e[x].val<k)
	  return e[ls1].siz+e[x].cnt+getrk(rs1,k);
	if(e[x].val>k)
	  return getrk(ls1,k);
}

按排名查询值

查询值的时候,比较的是 k 与当前点的排名,而当前点的排名就是左子树的大小加1,也就是 e[e[x].ls].siz+1 ,如果 k 包含在当前点里,由于当前点代表的是一个值,所以当前点的里面可能是多个值相同的点,也就是当k在当前点代表的值里面的其中一个点的时候,转成代码就是 e[e[x].ls].siz+1<=k 并且 e[e[x].ls].siz+e[x].cnt>=k,我们就可以直接返回当前点的值;而当 k<e[e[x].ls].siz+1 的时候,也就是说当前 k 的排名是小于当前排名的,我们就需要去左子树去寻找,而当 k>e[e[x].ls].siz+e[x].cnt 的时候,也就是说我们需要去当前点的右子树去寻找的时候,我们就会遇到上面遇到的问题,也就是当进行下一次递归的时候,我们查询的 k 的排名应该是整棵树的第 k 个,而到了右子树里面的时候,我们应该去查找的在右子树中的排名就不是 k 了,而是 k-e[e[x].ls].siz-e[x].cnt,因为由二叉搜索树的性质可知当前右子树中任意一个点在整棵树的排名就是他在右子树的排名+e[e[x].ls].siz+e[x].cnt,所以在递归的时候 k 要减去。

代码实现

inline int getval(int x,int k)
{
//	cout<<"FUCK"<<endl;
	int rkx=e[ls1].siz+1;
	if(rkx<=k&&rkx+e[x].cnt-1>=k)
	  return e[x].val;
	if(rkx>k)
	   return getval(ls1,k);
	if(rkx+e[x].cnt-1<k)
	  return getval(rs1,k-(rkx+e[x].cnt-1));
}

查询前驱或后继

查询前驱的时候,如果 k 的值是小于等于当前点的值的时候,我们就需要一直往左子树找,如果当前 k 的值大于当前点的值的话,说明 k 的前驱在当前点和上一个点之间内,也就是当前点的右子树,所以往右子树去尽量找小于 k 的最大值,查询后继也是同理。

代码实现

inline int getpre(int x,int k)
{
	int p=x,ans;
	while(p)
	{
		if(k<=e[p].val)p=e[p].ls;
		else ans=p,p=e[p].rs;
	}
	return e[ans].val;
}
inline int getnxt(int x,int k)
{
	int p=x,ans;
	while(p)
	{
		if(k>=e[p].val)p=e[p].rs;
		else ans=p,p=e[p].ls;
	}
	return e[ans].val;
}

当然这道题是平衡树模板题,是不会让你卡过去的,所以最后一个点是专门来卡BST的,因为上面也提到过,当数据极端的时候,BST可以退化成链,这样的话效率甚至不如数组。

插曲

x=++cnt打成x==++cnt调了1个多小时。。。
image

查询排名和值的时候把if里的k>=e[p].val打成了k>=e[x].val,半个小时

image

posted @ 2022-09-28 21:54  北烛青澜  阅读(64)  评论(0编辑  收藏  举报