指针实现 Treap

前置知识:二叉排序树,堆。

应用场景:平衡树。


〇、导入

1. 二叉排序树

我们都知道,二叉排序树就是满足“\(lch<now<rch\)”(左儿子小于根节点,右儿子大于根节点)的二叉树,一般情况下插入、删除和搜索的时间复杂度都为 \(\Theta(\log n)\) ,非常快。但在特殊情况下,二叉排序树可能会退化成链,时间复杂度也会变为 \(\Theta(n)\) 。只有当二叉排序树平衡时速度最优。

2. 堆

堆也很简单,就是根节点大于子节点的完全二叉树。

3. 平衡树

当二叉排序树退化成链时速度会大打折扣,因此很多毒瘤出题者都喜欢卡它。但是既然能有这种题目,那么肯定就有能解决的方法,当然也不排除出题者自己都没有做出来。二叉排序树平衡时时间复杂度为 \(\Theta(\log n)\) 。所以,若要保证二叉排序树的最大时间复杂度为 \(\Theta(\log n)\) ,就要保证该二叉排序树平衡。这就是是平衡树。

平衡树有很多种,本文就不再赘述,仅讨论 Treap

4. Treap

二叉排序树和堆都是二叉树。既然堆就是完全二叉树,何不利用它这个性质来保证二叉排序树平衡呢?

于是 Tree(二叉排序树)+Heap(堆)=TreapTreap 横空出世!

一、操作&实现

1. 存储

这里我定义了一个结构体 Treap 来封装,然后再 Treap 内又定义了结构体 node ,表示节点。

struct Treap
{
	struct node
	{
		int val,size,times;
		unsigned rd;
		node *ch[2];
	}e[100005],*root,*cnt;
};

解读:

  • Treap:表示 Treap
    • node:表示节点。
      • val:表示该节点所储存的数值。
      • size:表示以该节点为根的子树的大小(节点总数)。
      • times:表示该节点的数值的存在数量。
      • rd:随机优先值。
      • ch:指向子节点的指针。
        • ch[0]:指向左儿子的指针。
        • ch[1]:指向右儿子的指针。
    • e:储存所有节点。
    • root:指向根节点的指针。
    • cnt:指向最后一个插入的节点的指针。

如果没能看懂各变量的作用也没有关系,在后面的操作中会为您一一解答。

2. 操作

2-0 node 的操作

2-0-1 构造函数

node(){}
// 插入节点时用
node(const int &x){val=x,size=times=1,rd=rand(),ch[0]=ch[1]=NULL;}
优化

C++ 自带的 rand 函数速度较慢,我们可以自己写一个 mrand 函数来取代:

inline unsigned mrand()
{
	static unsigned long long tr=431322;
	return unsigned(tr=(tr*76717)%0x100000000);
}

大家可能 static 用得较少,这里就不阐述原理了,感兴趣的可以自行百度。

然后构造函数如下:

node(){}
// 插入节点时用
node(const int &x){val=x,size=times=1,rd=mrand(),ch[0]=ch[1]=NULL;}

2-0-2 更新节点信息

不解释:

inline void pushup()
{
	size=times;
	if(ch[0])	size+=ch[0]->size;
	if(ch[1])	size+=ch[1]->size;
}

现在 node 实现如下:

struct node
{
	int val,size,times;
	unsigned rd;
	node *ch[2];
	node(){}
	node(const int &x){val=x,size=times=1,rd=mrand(),ch[0]=ch[1]=NULL;}
	inline void pushup()
	{
		size=times;
		if(ch[0])	size+=ch[0]->size;
		if(ch[1])	size+=ch[1]->size;
	}
};

下面实现操作的函数均为 Treap 的成员函数。

附:调试函数,输出当前树的详细信息:

// now 表示所操作子树的根节点指针引用,indent 表示当前缩进长度
void output(node *&now,const int &indent)
{
	putchar('>'),putchar(' ');
	for(int i=0;i<indent;++i)	putchar('|'),putchar(' ');
	if(!now)
	{
		puts("NULL");
		return;
	}
	// 输出当前节点的详细信息,可以更改
	printf("%ld:%d %d,%d %u\n",now-e,now->val,now->size,now->times,now->rd);
	output(now->ch[0],indent+1);
	output(now->ch[1],indent+1);
}
// 初始函数(方便调用)
inline void output(){output(root,0);}

2-1 旋转

旋转是很多平衡树常见的操作,分为左旋和右旋。

左旋的操作如下:


右旋的操作与此类似,仅方向不同。事实上,右图中的树右旋后即可得到左图。

实现的代码也很简单:

// now 表示所操作子树的根节点指针引用,d 表示旋转方向(0 表示右旋,1 表示左旋)
// 这里 now 为引用类型,便于更改。
inline void rotate(node *&now,const bool &d)
{
	node *tmp=now->ch[d];			// tmp 指向将成为新的根节点的节点
	now->ch[d]=tmp->ch[!d];
	tmp->ch[!d]=now;
	tmp->pushup(),now->pushup();	// 更新节点信息
	now=tmp;						// tmp 指向的节点成为新的根节点
}

那么为什么要旋转呢?因为每个节点都会有一个随机优先值,而 Treap 的每个节点的优先值都比其子节点的大,利用堆的思想,使得 Treap 相对平衡。

2-2 插入值为 \(x\) 的节点

Treap 的插入其实就是在二叉排序树的插入的基础上通过旋转保证 Treap 堆的性质。

// now 表示指向当前节点的指针的引用,x 表示要插入的值
// 这里 now 为引用类型,便于更改。
void insert(node *&now,const int &x)
{
	// 如果为空指针
	if(!now)
	{
		*(now=++cnt)=x;		// 插入新节点
		return;
	}
	++(now->size);			// 因为新节点在以 now 指向的节点为根节点的子树内,所以当前子树的节点数+1
	// 如果当前节点的数值不等于 x
	if(now->val!=x)
	{
		bool d=now->val<x;	// d 为插入的方向(0 为左,1 为右)
		insert(now->ch[d],x);
		// 如果当前节点的子节点的优先值大于当前节点的优先值(即不符合堆的性质)
		if(now->ch[d]->rd>now->rd)	rotate(now,d);	// 旋转以维护堆的性质
	}
	// 否则说明当前节点的数值等于 x
	else	++(now->times);	// 当前节点的数值的存在数量+1
	now->pushup();			// 更新当前节点(之前我没有加上,导致我调了好久的 BUG)
}
// 初始函数(方便调用)
inline void insert(const int &x){insert(root,x);}

2-3 删除值为 \(x\) 的节点

首先找到要删除的节点,然后通过旋转将其下移,直到其没有子节点时之间再将其直接删除。

// now 表示指向当前节点的指针的引用,x 表示要删除的值
// 这里 now 为引用类型,便于更改。
void remove(node *&now,const int &x)
{
	// 如果当前节点不为要删除的节点
	if(now->val!=x)	remove(now->ch[now->val<x],x);	// 继续向下寻找
	// 否则说明要删除当前节点
	// 如果当前节点有左儿子并且左儿子的优先值大于右儿子的优先值
	// 则右旋使左儿子代替被删除的节点的位置
	else if(now->ch[0] && (!now->ch[1] || now->ch[0]->rd>now->ch[1]->rd))	rotate(now,0),remove(now->ch[1],x);
	// 否则如果当前节点有右儿子
	// 则左旋使右儿子代替被删除的节点的位置
	else if(now->ch[1])	rotate(now,1),remove(now->ch[0],x);
	// 否则说明当前节点没有子节点
	else
	{
		--(now->size);
		// 如果该节点的存在数量清零了
		if(!--(now->times))	now=NULL;	// 直接删除
		return;
	}
	now->pushup();// 更新当前节点
}
// 初始函数(方便调用)
inline void remove(const int &x){remove(root,x);}

2-4 查询数值为 \(x\) 的节点的排名

这个与二叉排序树的操作一样。

// now 表示指向当前节点的指针的引用,x 表示要查询的值
int getrank(node *&now,const int &x)
{
	// 如果为空指针,说明改数不存在于树中
	if(!now)	return 0;							// 直接返回 0
	// 如果当前节点的值大于 x
	if(now->val>x)	return getrank(now->ch[0],x);	// 继续搜索左子树
	// 如果当前节点的值小于 x
	// 继续搜索右子树,返回值增加左子树节点数+当前节点存在数量
	if(now->val<x)	return getrank(now->ch[1],x)+(now->ch[0]?now->ch[0]->size:0)+now->times;
	// 否则说明当前节点的值等于 x
	return (now->ch[0]?now->ch[0]->size:0)+1;		// 返回左子树节点数+1
}
// 初始函数(方便调用)
inline int getrank(const int &x){return getrank(root,x);}

2-5 查询排名为 \(x\) 的节点的数值

// now 表示指向当前节点的指针的引用,x 表示要查询的排名
int getval(node *&now,const int x)
{
	static int tmp;	// 临时变量,用于储存左子树的节点数+当前节点存在数量,为了节省空间就使用静态变量了
	// 如果当前节点有左儿子且左子树的节点数不小于 x
	// 继续搜索左子树
	if(now->ch[0] && now->ch[0]->size>=x)	return getval(now->ch[0],x);
	// 否则如果当前节点有右儿子且左子树的节点数+当前节点存在数量小于 x
	// 继续搜索右子树中排名为 x-tmp 的节点
	if(now->ch[1] && ((tmp=(now->ch[0]?now->ch[0]->size:0)+now->times)<x))	return getval(now->ch[1],x-tmp);
	// 否则说明当前节点即要查询的节点
	return now->val;// 返回当前节点的数值
}
// 初始函数(方便调用)
inline int getval(const int &x){return getval(root,x);}

2-6 查询数值为 \(x\) 的节点的前驱

// now 表示指向当前节点的指针的引用,x 表示要查询的数值
int getprev(node *&now,const int &x)
{
	// 如果为空指针
	if(!now)	return -0x80000000;						// 返回负无穷
	// 如果当前节点的数值不小于 x
	// 说明前驱在左子树内
	if(now->val>=x)	return getprev(now->ch[0],x);		// 搜索左子树
	// 否则说明前驱为当前节点或在右子树内
	else	return max(now->val,getprev(now->ch[1],x));	// 搜索右子树
}
// 初始函数(方便调用)
inline int getprev(const int &x){return getprev(root,x);}

2-7 查询数值为 \(x\) 的节点的后继

与查询前驱思路相同。

// now 表示指向当前节点的指针的引用,x 表示要查询的数值
int getnext(node *&now,const int &x)
{
	// 如果为空指针
	if(!now)	return 0x7fffffff;						// 返回负无穷
	// 如果当前节点的数值不大于 x
	// 说明后继在左子树内
	if(now->val<=x)	return getnext(now->ch[1],x);		// 搜索左子树
	// 否则说明后继为当前节点或在右子树内
	else	return min(now->val,getnext(now->ch[0],x));	// 搜索右子树
}
// 初始函数(方便调用)
inline int getnext(const int &x){return getnext(root,x);}

二、例题

1. 洛谷 P3369 【模板】普通平衡树

题目链接:https://www.luogu.com.cn/problem/P3369

这是一道模板题,没什么好说的,直接上代码:

posted @ 2020-03-17 19:31  Createsj  阅读(425)  评论(0编辑  收藏  举报