前置知识:二叉排序树,堆。
应用场景:平衡树。
〇、导入
1. 二叉排序树
我们都知道,二叉排序树就是满足“(lch<now<rch)”(左儿子小于根节点,右儿子大于根节点)的二叉树,一般情况下插入、删除和搜索的时间复杂度都为 (Theta(log n)) ,非常快。但在特殊情况下,二叉排序树可能会退化成链,时间复杂度也会变为 (Theta(n)) 。只有当二叉排序树平衡时速度最优。
2. 堆
堆也很简单,就是根节点大于子节点的完全二叉树。
3. 平衡树
当二叉排序树退化成链时速度会大打折扣,因此很多毒瘤出题者都喜欢卡它。但是既然能有这种题目,那么肯定就有能解决的方法,当然也不排除出题者自己都没有做出来。二叉排序树平衡时时间复杂度为 (Theta(log n)) 。所以,若要保证二叉排序树的最大时间复杂度为 (Theta(log n)) ,就要保证该二叉排序树平衡。这就是是平衡树。
平衡树有很多种,本文就不再赘述,仅讨论 Treap
。
4. Treap
二叉排序树和堆都是二叉树。既然堆就是完全二叉树,何不利用它这个性质来保证二叉排序树平衡呢?
于是 Tree
(二叉排序树)+Heap
(堆)=Treap
,Treap
横空出世!
一、操作&实现
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
",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 。
这是一道模板题,没什么好说的,直接上代码: