Treap平衡树学习笔记


放在前面的话

与信息学竞赛告别9个月后,中考终于结束,我终于复出了。。。
结果一回来就学数据结构,学得我有点懵。。。
本文写了两个多星期。。。
尽管学习Treap感到万分痛苦!但是!

文章目录

  • Treap平衡树学习笔记
    • 放在前面的话
    • 人类的本质是复读机,Treap的本质是二叉搜索树
    • Treap杀手锏——自动平衡
      • 为什么要平衡?
      • 树上旋转!!!
      • 自动平衡!!!
      • 小结
    • Treap操作
      • 定义Treap
      • 旋转
      • 插入
      • 删除
      • 维护子树大小
      • 求排名为kkk的元素
      • 求元素xxx的排名
      • 求元素的前驱后继
    • 你以为学完Treap了?不!还有FHQ Treap
      • 无需旋转的Treap
      • 超强核心Merge
      • ~~另一核心Split~~万物皆基于Merge
        • 分裂Split
        • 插入Insert
        • 删除Delete
      • 小结×2\times2×2
    • 放在最后的话

人类的本质是复读机,Treap的本质是二叉搜索树

想要了解Treap,首先来看看它的本质——二叉搜索树:
二叉搜索树(Binary Search Tree),又叫二叉查找树、二叉排序树、BST,这是一种具备特殊性质的二叉树。

BST的性质

  1. 若任意结点的左子树不空,则左子树上所有结点的值均不大于它的根结点的值。
  2. 若任意结点的右子树不空,则右子树上所有结点的值均不小于它的根结点的值。
  3. 任意结点的左、右子树也分别为二叉搜索树。

这是一种将数据有序化的结构,如果我们对它进行中序遍历并在遍历过程中输出每一个结点,我们就能得到一个按降序排好序的数列。
二叉搜索树能够高效地进行如下操作:

  1. 插入一个数值
  2. 查询是否包含某个数值
  3. 删除某个数值

由于BST并非本文重点,这里仅仅简洁介绍与本文相关的内容。
然后我们进入正题:Treap平衡树


Treap杀手锏——自动平衡

为什么要平衡?

不知在运用BST的时候,大家有没有遇到过这种情况:

在不好运的时候,要储存的整个序列中,较小或较大的数作为二叉搜索树的根结点。此时,BST的结构会发生偏沉,即偏向某一边,甚至使BST退化为一条链。

这将使原本单次操作期望时间复杂度Θ(log⁡n)\Theta(\log n)Θ(logn)退化为O(n)O(n)O(n),极大降低这种数据结构工作的效率,使BST的优势消失殆尽。

Treap是基于BST的优化数据结构,其自动平衡的优化特性就能解决树的偏沉的问题,使其期望树高为O(log⁡n)O(\log n)O(logn),从而优化操作的效率,保持单次操作期望的时间复杂度为Θ(log⁡n)\Theta (\log n)Θ(logn)

那么Treap是怎样保持二叉搜索树的平衡的呢?

答案在于:旋转Rotate!!!


树上旋转!!!

我们知道,二叉树如果出现偏沉,不是往左偏就是往右偏,所以对应这两种情况,我们分别使用右旋转左旋转两种操作使二叉树平衡。
对于左偏的情况,我们将其右旋转:

根据上图,我们可以知道右旋转的基本步骤:

  1. 根降为右子树的根,左儿子代替原根
  2. 由于原左儿子的右儿子要设置为原根,又因为左子树中所有结点值都小于原根,所以原左儿子的右儿子设置为原根的左儿子
  3. 原左儿子的右儿子设置为原根
void RightRotate(node *root)
{node *tmp = root->leftson;//用tmp暂存根的左儿子root->leftson = tmp->rightson;//由于根的左儿子将代替根的位置,//根始终大于左子树的所有结点,所以,根的左结点设置为其左儿子的右儿子tmp->rightson = root;//左儿子的右儿子设置为根root = tmp;//根正式降为子树中右子树的根,原左儿子正式取代原根的位置return ;//右转完成!!!
}

同理,极其容易得到左旋转的步骤,无需赘述,哈哈哈
好!那么左旋转也是类似的:

同样,根据上图,我们可以知道左旋转的基本步骤:

  1. 根降为左子树的根,右儿子代替原根
  2. 由于原右儿子的左儿子要设置为原根,又因为右子树中所有结点值都大于原根,所以原右儿子的左儿子设置为原根的右儿子
  3. 原右儿子的左儿子设置为原根
void LeftRotate(node *root)
{node *tmp = root->rightson;//用tmp暂存根的右儿子root->rightson = tmp->leftson;//由于根的右儿子将代替根的位置,//根始终小于右子树的所有结点,所以,根的右结点设置为其右儿子的左儿子tmp->leftson = root;//右儿子的左儿子设置为根root = tmp;//根正式降为子树中左子树的根,原右儿子正式取代原根的位置return ;//左转完成!!!
}

这下旋转绕得可有点晕!不理解的同学可以自己动手画一画感受一下!
通过上面的演示,我们可以感受到:旋转减小了树的高度,有效控制树的高度就能提高我们遍历的效率。同时,我们可以总结出树上旋转的一些性质:

旋转的性质 1

  • 左旋一个子树,会把它的根节点旋转到根的左子树位置,同时根节点的右子节点成为子树的根
  • 右旋一个子树,会把它的根节点旋转到根的右子树位置,同时根节点的左子节点成为子树的根。

左旋后的根节点降到了左子树,右旋后根节点降到了右子树,而且仍然满足 BST 性质,于是有:

旋转的性质 2

  • 对子树旋转后,子树仍然满足 BST 性质。

有了旋转,我们就能使二叉树平衡啦!但是,新的问题随之而来:程序怎么判断什么时候旋转?怎么做到自动平衡的效果呢?


自动平衡!!!

为了尽量保持树的平衡,当然是在每一次修改操作的时候都旋转一下啦!为了防止程序一味沉迷于左旋或右旋,我们引入一个叫修正值的东西来指导程序旋转

我们给树上的每个结点都设置一个修正值,并且要求程序在保证结点上的值满足BST的性质下,同时结点上的修正值满足最小堆性质1。就像这个样子:

我们让一个结点修正值取一个随机数,它有可能大于或小于根结点的修正值(等于的情况此处暂不讨论),为了满足最小堆性质,当子结点修正值小于根结点的修正值时,就要求程序将其旋转,以保持树的平衡
修正值的值是随机生成的,这意味着出现有序的随机序列是小概率事件,即树中偏沉或退化成链的可能性大大减小,随机出现的左旋、右旋、不旋实现了Treap的自动平衡,所以 Treap 的结构是趋向于随机平衡的。

由此可知,旋转的意义在于:
旋转可以使不满足堆序的两个节点通过调整位置,重新满足堆序,而不改变 BST 性质。


小结

综上所述,我们可以将Treap归纳总结为:
Treap=Tree+Heap
二叉查找树(BST)+满足 最小堆性质 的修正值
Treap 可以定义为有以下性质的二叉树:

Treap基本性质

  1. 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值,而且它的根节点的修正值小于等于左子树根节点的修正值;
  2. 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值,而且它的根节点的修正值小于等于右子树根节点的修正值;
  3. 它的左、右子树也分别为 Treap。

修正值是节点在插入到 Treap 中时随机生成的一个值。
为了使 Treap 中的节点同时满足 BST 性质和最小堆性质,不可避免地要对其结构进行调整,调整方式被称为旋转。在维护 Treap 的过程中,只有两种旋转,分别是左旋转(简称左旋)和右旋转(简称右旋)。


Treap操作

定义Treap

struct Treap_Node{Treap_Node *lson,*rson;//左儿子,右儿子 //也可方便表示为 Treap_Node *son[2];//son[0]左儿子son[1]右儿子int val;//值int key;//修正值;下面的fix\fkey均代表修正值//子树大小即子树的结点总数,后面会用到int siz;//以当前结点为根的子树大小;下面的tsize同样代表字数大小void getsize()//统计子树大小;调用方式:p->getsize();{siz = 1;if(lson) siz += lson->siz;//左儿子不为空就加上它的子树大小if(rson) siz += rson->siz;//右儿子不为空就加上它的字数大小}
}*root,mem[233333]/*内存池*/,*now = mem;

旋转

稍复杂

void leftrotate(Treap_Node *&p)
{Treap_Node *tmp = p->rson;//暂存根节点的右儿子 p->rson = tmp->lson;//根节点的右儿子变为其右儿子的左儿子 tmp->lson = p;//其右儿子的左儿子变为根节点,根节点降为其右儿子的左子树的根节点 getsize(p);//若需要统计,在此处记得更新!!!!!!!p = tmp;//将右儿子变为根节点 getsize(p);//若需要统计,在此处记得更新!!!!!!!
}void rightrotate(Treap_Node *&p)
{Treap_Node *tmp = p->lson;//暂存根节点的左儿子 p->lson = tmp->rson;//根节点的左儿子变为其左儿子的右儿子 tmp->rson = p;//其左儿子的右儿子变为根节点,根节点降为其左儿子的右子树的根节点 getsize(p);//若需要统计,在此处记得更新!!!!!!!p = tmp;//将左儿子变为根节点 getsize(p);//若需要统计,在此处记得更新!!!!!!!}

用01代替的简便函数

void Rotate(node *&p,int k)//k=0:左转 k=1:右转
{node *tmp = p->son[(k^1)];p->son[(k^1)] = tmp->son[k];tmp->son[k] = p;p->getsize();//若需要统计,在此处记得更新!!!!!!!p = tmp;p->getsize();//若需要统计,在此处记得更新!!!!!!!return ;
}

插入

具体步骤:

  1. 从根结点开始遍历
  2. 如果插入的值比当前结点的值小,为满足BST性质,往左子树遍历
  3. 如果插入的值比当前结点的值大,为满足BST性质,往右子树遍历
  4. 如果相同,则在当前结点计数
  5. 如果找到空位置,就在空位置创建一个结点插入

创建一个结点的创建函数

Treap_Node *newnode(int num)//下面方便添加结点
{Treap_Node *a = now++;/*分配指针内存*/a->lson = a->rson = NULL;a-> val = num;a-> key = rand();//随机函数;修正值随机赋值a->siz = 1;return a;
}

插入函数简洁版

void Insertx(posnode *&p,int num)//维护记录本编码的书的位置的平衡树
{if(p == NULL)//找到位置就放 {p = newnode(num);        return ;}int k = where > p->pos;//书的位置小于当前取0,大于当前取1 Insertx(p->son[k] , num);//继续找空位p->getsize();//若需要统计,在此处记得更新!!!!!!!if(p->key > p->son[k]->key)//破坏堆序就旋转 Rotate(p,(k^1));return ;
}

烦杂版

void insertx(Treap_Node *&p,int num)
{if(p == NULL)//当前节点为空,可插入节点 {p = newnode(num);//新建一个节点 return ;}if(num < p->val)//当前值比根节点小,在其左子树找可放置的节点 {   insertx(p->lson,num);getsize(p);//若需要统计,在此处记得更新!!!!!!!if(p->lson->fkey < p->fkey) rightrotate(p);//若生成的修正值破坏了最小堆性质,右旋维护树的平衡 }else{insertx(p->rson,num);getsize(p);//若需要统计,在此处记得更新!!!!!!!if(p->rson->fkey < p->fkey) leftrotate(p);return ;}
}

删除

既然学了旋转,那么就要用好旋转。是吧! 当一个结点只有一个子结点或没有子结点时,删除它是最容易的。当只有一个子结点时,用这个子结点直接取代其根结点;当没有子结点时,就直接删除这个结点。为了使目标结点只有一个子结点或没有子结点,我们可以利用旋转,将目标结点“转”到方便删除的位置。

  1. 寻找目标结点
  2. 找到目标结点判断情况:
    Ⅰ.Ⅰ.Ⅰ.没有子结点:直接删除
    Ⅱ.Ⅱ.Ⅱ.有一个子结点:用子结点代替
    Ⅲ.Ⅲ.Ⅲ.有两个子结点:比较左右儿子的修正值的大小,修正值小的做根保证
    \space \space \space\space\space     满足最小堆性质,旋转根结点

烦杂版

void deletex(Treap_Node *&p,int num)
{if(p == NULL) return ;//该节点为空节点,返回上一级继续寻找 if(p->val == num)//找到要删除的节点 {//      if(p->sum > 1)
//      {//          p->sum--;
//          return ;
//      } if((p->lson)&&(p->rson))//有左右儿子 {if(p->lson->fkey < p->rson->fkey)//左儿子修正值更小,右旋{rightrotate(p);deletex(p->rson,num);//继续删除 }   else//否则左旋 {leftrotate(p); deletex(p->lson,num);}getsize(p);//若需要统计,在此处记得更新!!!!!!!}else{if((!p->lson)&&(!p->rson)) p = NULL;//作为叶子结点时,直接删除 else{if(p->lson) p = p->lson;//用左儿子代替 else p = p->rson;//用右儿子代替 }}}else//没有找到要删的元素 {if(num < p->val) deletex(p->lson,num);else deletex(p->rson,num);getsize(p);//若需要统计,在此处记得更新!!!!!!!}
}

简洁版

void Deletex(posnode *&p,int num)
{if(p == NULL) return ;//空即返回 if(p->val == num)//找到目标结点{if(p->son[1]&&p->son[0])//有两个儿子 {int k = p->son[0]->key < p->son[1]->key;//如果左儿子修正值>右儿子修正值就右旋,否则左旋 Rotate(p ,k);Deletex(p->son[k],num);p->getsize(); //若需要统计,在此处记得更新!!!!!!!}else{if(p->son[1])//只有右儿子 p = p->son[1];else{if(p->son[0])//只有左儿子 p = p->son[0];else//没有儿子直接删除 p = NULL;}}return ;}else{int k = num > p->val;Deletex(p->son[k],num);//继续找位置 p->getsize();若需要统计,在此处记得更新!!!!!!! }
}

维护子树大小

统计以某一结点为根的子树的所有结点数
直接更新,减少操作时间,但是要常常调用,特别在旋转、插入、删除之后一定要调用,详细请观察其他片段代码中,标有 若需要统计,在此处记得更新!!!!!! 的字段位置

struct Treap_Node{......void getsize()//统计子树大小;调用方式:p->getsize();{siz = 1;if(lson) siz += lson->siz;//左儿子不为空就加上它的子树大小if(rson) siz += rson->siz;//右儿子不为空就加上它的字数大小}}
void getsize(Treap_Node *&p)//等价于结构体内的函数;调用方式:getsize(p)
{p->siz = 1;if(p->lson) p->siz += p->lson->siz;//左儿子不为空就加上它的子树大小if(p->rson) p->siz += p->rson->siz;//右儿子不为空就加上它的字数大小
}

求排名为kkk的元素

求排名为kkk的元素就是问在一序列中第kkk小的元素是哪一个。
很显然根据BST的性质,我们是这样数的:

通过上图我们可以发现:
一部分结点的排名直接取决于它左子树的大小,如1,2,4,6
还有一部分结点不仅要看它左子树大小,还要看前面的结点数,如3,5,7

再仔细分析可得:

  1. 我们从任意结点出发求一个对于这棵子树排名kkk,设其左子树大小lsizelsizelsize,其自身包含的元素个数mmm
  2. 若lsize≥mlsize \geq mlsize≥m,那么排名kkk的元素一定在其左子树里,且新排名为kkk
  3. 若lsize+m<klsize +m<klsize+m<k,那么排名kkk的元素一定在其右子树里,且新排名为k−lsize−mk-lsize-mk−lsize−m
  4. 若lsize+1≤k且k≤lsize+mlsize +1 \leq k且k\leq lsize+mlsize+1≤k且k≤lsize+m,那么这一个结点的值就是排名第kkk的元素的值

由上面的步骤,我们可以想到用递归来实现求排名第kkk的元素的算法,所以可得以下代码。

int getkth(Treap_Node *&p,int k)//找排名为k的元素
{int lsize = 0 ;if(p->lson) lsize = p->lson->tsize;//左子树大小 if(k == lsize + 1) return p->val;//找到返回 if(k <= lsize ) return getkth(p->lson, k );//是其左子树的第k名 else return getkth(p->rson , k-lsize-1);//是其右子树的k-lsize-1名
}

求元素xxx的排名

求元素xxx的排名其实相当于是求排名kkk元素的互逆运算。
这里采用更简单的方法:直接数有多少个数小于xxx最后加上1就是xxx的排名了。

int getrank(Treap_Node *&p,int num)//找到x的排名,=找有多少个元素小于x
{if(p == NULL) return 0;//空节点不计数 if(p->val >= num) return getrank(p->lson, num);//去左子树算排名 int lsize=0;if(p->lson) lsize=p->lson->tsize;return lsize + getrank(p->rson, num) + 1;//返回排名 }

求元素的前驱后继

在这里,我们定义:
一个元素xxx的前驱,是在平衡树中,不大于xxx的元素中最大的元素。
相似地,一个元素xxx的后继,是在平衡树中,不小于xxx的元素中最小的元素。
因此,在遍历树的过程中,比较并记录就好了。

int getpre(Treap_Node *p,int num)//求前驱
{if(p == NULL) return -1;if(p->val >= num) return getpre(p->lson , num);//当前结点大于元素,往左找int tmp = getpre(p->rson, num);//否则往右找if(tmp!=-1) return tmp;else return p->val;
}int getsuc(Treap_Node *p,int num)//求后继
{if(p == NULL) return -1;if(p->val <= num) return getsuc(p->rson, num );int tmp = getsuc(p->lson, num);if(tmp!=-1) return tmp;else return p->val;
}

模板题

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<ctime>using namespace std;#define maxn 99999999999int n,opt,x;
struct Treap_Node{Treap_Node *lson,*rson;//左儿子、右儿子 int val;//值 int fkey;//修正值 int tsize;//树的大小
//  int sum;//同一值出现的次数
}memory_pool[2333333],*now = memory_pool,*root;void getsize(Treap_Node *&p)//更新树的大小
{p->tsize=1;if(p->lson) p->tsize += p->lson->tsize;if(p->rson) p->tsize += p->rson->tsize;} Treap_Node *newnode(int num)//新建一个节点
{Treap_Node *tmp = now++;tmp->val = num;tmp->lson = tmp->rson = NULL;//初始化为空 tmp->fkey = rand();//随机取修正值 tmp->tsize = 1;//初始树大小为1 return tmp;
}void leftrotate(Treap_Node *&p)
{Treap_Node *tmp = p->rson;//暂存根节点的右儿子 p->rson = tmp->lson;//根节点的右儿子变为其右儿子的左儿子 tmp->lson = p;//其右儿子的左儿子变为根节点,根节点降为其右儿子的左子树的根节点 getsize(p);p = tmp;//将右儿子变为根节点 getsize(p);
}void rightrotate(Treap_Node *&p)
{Treap_Node *tmp = p->lson;//暂存根节点的左儿子 p->lson = tmp->rson;//根节点的左儿子变为其左儿子的右儿子 tmp->rson = p;//其左儿子的右儿子变为根节点,根节点降为其左儿子的右子树的根节点 getsize(p);p = tmp;//将左儿子变为根节点 getsize(p);}void insertx(Treap_Node *&p,int num)
{if(p == NULL)//当前节点为空,可插入节点 {p = newnode(num);//新建一个节点 return ;}if(num < p->val)//当前值比根节点小,在其左子树找可放置的节点 {   insertx(p->lson,num);getsize(p);if(p->lson->fkey < p->fkey) rightrotate(p);//若生成的修正值破坏了最小堆性质,右旋维护树的平衡 }else{insertx(p->rson,num);getsize(p);if(p->rson->fkey < p->fkey) leftrotate(p);return ;}
}void deletex(Treap_Node *&p,int num)
{if(p == NULL) return ;//该节点为空节点,返回上一级继续寻找 if(p->val == num)//找到要删除的节点 {//      if(p->sum > 1)
//      {//          p->sum--;
//          return ;
//      } if((p->lson)&&(p->rson))//有左右儿子 {if(p->lson->fkey < p->rson->fkey)//左儿子修正值更小,右旋{rightrotate(p);deletex(p->rson,num);//继续删除 }   else//否则左旋 {leftrotate(p); deletex(p->lson,num);}getsize(p);}else{if((!p->lson)&&(!p->rson)) p = NULL;//作为叶子结点时,直接删除 else{if(p->lson) p = p->lson;//用左儿子代替 else p = p->rson;//用右儿子代替 }}}else//没有找到要删的元素 {if(num < p->val) deletex(p->lson,num);else deletex(p->rson,num);getsize(p);}
}int getrank(Treap_Node *&p,int num)//找到x的排名,=找有多少个元素小于x
{if(p == NULL) return 0;//空节点不计数 if(p->val >= num) return getrank(p->lson, num);//去左子树算排名 int lsize=0;if(p->lson) lsize=p->lson->tsize;return lsize + getrank(p->rson, num) + 1;//返回排名 }int getkth(Treap_Node *&p,int k)//找排名为k的元素
{int lsize = 0 ;if(p->lson) lsize = p->lson->tsize;//左子树大小 if(k == lsize + 1) return p->val;//找到返回 if(k <= lsize ) return getkth(p->lson, k );//是其左子树的第k名 else return getkth(p->rson , k-lsize-1);//是其右子树的k-lsize-1名
} int getpre(Treap_Node *p,int num)
{if(p == NULL) return -1;if(p->val >= num) return getpre(p->lson , num);int tmp = getpre(p->rson, num);if(tmp!=-1) return tmp;else return p->val;
}int getsuc(Treap_Node *p,int num)
{if(p == NULL) return -1;if(p->val <= num) return getsuc(p->rson, num );int tmp = getsuc(p->lson, num);if(tmp!=-1) return tmp;else return p->val;
}int main()
{srand((unsigned)time(NULL));scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%d%d",&opt,&x);switch(opt){case 1:insertx(root,x);break;case 2:deletex(root,x);break;case 3:printf("%d\n",getrank(root,x)+1);break;case 4:printf("%d\n",getkth(root,x));break;case 5:printf("%d\n",getpre(root,x));break;case 6:printf("%d\n",getsuc(root,x));break;}}return 0;
}

你以为学完Treap了?不!还有FHQ Treap

无需旋转的Treap

普通Treap通过旋转来保持自身的平衡,然而,它也有编程复杂,难以支持区间操作的缺点。为了使Treap更加强大,一位叫FHQ的大神发明了一种编写更简易,功能更强大,能够实现可持久化的Treap,这种Treap的一大特点就是:不需要旋转!
它的基本原理和普通Treap并没有很大差异,也是通过随机值来使Treap自动平衡,不过这一次,我们把修正值改叫为优先级,把修正值满足最小堆性质改为优先级高的做根原则,从而方便核心操作的编写。
对于无旋Treap的核心操作,有两种:

  1. 支持两棵树的合并Merge
  2. 支持树的拆解和分裂Split

超强核心Merge

刚刚说无旋Treap有两大核心操作,现在我们就来看看超级万能的合并Merge操作。
它的功能是:给定两棵树,分别设为TLT_LTL​,TRT_RTR​,并保证TLT_LTL​中的所有元素都比TRT_RTR​中的所有元素要小,将这两棵树合并成新树,并使新树满足BST性质和优先级最大堆性质。
Merge的核心思想是一种类似分治的递归合并,目的就是保证将两棵树合并后,满足其优先级最大堆性质,使这棵树保持平衡。
具体步骤如下:

  1. 如果某一棵树为空,则返回另一棵树
  2. 比较其根结点的优先级,如果TLT_LTL​的优先级大,就递归,将其右子树和TRT_RTR​合并,并返回新的TLT_LTL​右子树;将TLT_LTL​的右子树更新之后,返回TLT_LTL​;
  3. 否则就递归,将TRT_RTR​的左子树和TLT_LTL​合并,并返回新的TRT_RTR​左子树;将TRT_RTR​的左子树更新后,返回TRT_RTR​

详细过程见下图:

代码实现

Treap_Node *Merge(Treap_Node *L,Treap_Node *R)//将两棵树合并,为保持BST性质,在输入时应当确保L中的值永远比R中的值小
{if(!L) return R;//某一棵树为空,返回另一棵树if(!R) return L;if(L->prior > R->prior)//比较优先级,优先级高的作为新根{L->rson = Merge(L->rson , R);//合并右子树和R//L->getsize();//更新树的大小return L;}else{R->lson = Merge( L, R->lson);//合并左子树和L//R->getsize();return R;}
}

这个代码是不是比旋转要简洁?合并操作理解起来是不是比旋转要简单?答案显而易见!
这段短短的代码可是FHQ Treap的核心!接下来我们就看看它的神奇之处!


另一核心Split万物皆基于Merge

额… …不好意思,虽然FHQ Treap的核心操作有两个,但是根据LH导师的指导和自己的实践经验,丧心病狂的 我成功地用Merge实现了Split的操作,将无旋Treap的核心操作减少到1个:Merge (哈哈哈!) 现在,我们就把Treap中的基本操作用Merge全部实现吧!

分裂Split

功能需求:给定一棵树TTT,要求拆分此树,将其前kkk个元素组成一棵树,剩下的元素组成另一棵树。
具体步骤:

  1. 设两棵新树T1T_1T1​,T2T_2T2​都为空,遍历原树
  2. 如果当前元素排名大于kkk,那就递归去往左子树分裂,回来以后将根结点和右子树用Merge加入到T2T_2T2​中去。
  3. 如果当前元素排名小于kkk,那就递归去往右子树,看看能不能继续分裂,回来以后将根结点和左子树用Merge将其加入T1T_1T1​。
  4. 如果当前元素排名为kkk,停止遍历,将它本身和左子树用Merge将其加入T1T_1T1​,右子树用Merge加入T2T_2T2​,结束递归。

代码如下:

/*
无旋Treap另一核心操作
基于Merge的Split操作
*/
void Split(Treap_Node *&T,int k,Treap_Node *&newt/*new tree*/,Treap_Node *&remt/*remain tree*/)//把一棵树,拆成由前k个数组成的树和剩下部分
{int lsize = 0;//有可能没有左儿子,要先判断!!! if(T->lson) lsize = T->lson->siz;//用于计算排名if(lsize >= k)//左子树大小比k大,第k个数一定在左子树里面{Split(T->lson , k , newt, remt);//在左子树拆树 T->lson = NULL;remt = Merge(remt,T);//将其本身和右子树并入右树 if(remt) remt->getsize();return ;} if(lsize + 1 < k)//在新树范围内,但还没到最后一个结点{Split(T->rson,k - lsize - 1,newt,remt);//在右子树继续拆树 T->rson = NULL; //因为已经在右子树拆过树,newt中的所有结点都来自于T右子树,又根据BST性质,所以newt中的所有的值 都比 当前没有右儿子的T中的所有的值要大 newt = Merge(T ,newt );//把其本身左子树全部合并if(newt) newt->getsize();return ;} if(lsize + 1 == k)//找到最后一个结点拆树到此为止{Treap_Node *tmp = T->rson;T->rson = NULL;newt = Merge(newt,T);//左儿子和它本身并入新树 newt->getsize();remt = Merge(remt,tmp);//右儿子并入旧树 remt->getsize();return ;}
}

插入Insert

算法流程:

  1. 如果当前有空位,就插入元素。
  2. 如果xxx小于当前根结点,去左子树找空位;回来以后,此时左子树已更新,就先切断原来的左子树,用Merge将根和右子树与左子树合并。
  3. 类似地,如果xxx大于当前根结点,去右子树找空位;回来以后,此时右子树已更新,就先切断原来的右子树,用Merge将根和左子树与右子树合并。
//基于Merge的Insert操作
Treap_Node *Insertx(Treap_Node *p,int num)
{if(p == NULL ) return newnode(num);if(num < p->val){Treap_Node *newson = Insertx(p->lson,num);//小于当前值往左走,并储存新的左子树p->lson = NULL;//切断旧的左子树p->getsize();//记得更新return Merge(newson,p);//合并新左子树和原树}else{Treap_Node *newson = Insertx(p->rson,num);//大于当前值往右走,并储存新的右子树p->rson = NULL;//切断旧的右子树p->getsize();//记得更新return Merge(p,newson);//合并新右子树和原树}
}

删除Delete

原本应该是基于Split和Merge的,现在被我魔改成基于Merge了。。。
算法流程:

  1. 如果找到要删除的点,就合并它的左右儿子作为新树,不必判断是否为空,Merge自动搞定,然后返回新树。
  2. 如果小于当前结点,往左子树继续找
  3. 如果大于当前结点,往右子树继续找
//基于Merge的Delete操作
Treap_Node *Deletex(Treap_Node *p,int num)
{if(p == NULL) return p;if(num == p->val) return Merge(p->lson,p->rson);//合并两棵子树,直接返回新树,从而删除节点if(num < p->val){p->lson = Deletex(p->lson,num);//往左找,更新左子树p->getsize();return p;}else{p->rson = Deletex(p->rson,num);//往左找,更新右子树p->getsize();return p;}
}

小结×2\times2×2

呼!快讲完了吧。。。好吧好吧,基本操作讲完了,其他的求排名之类的就与Merge没太大关系了,剩下的就纯粹是递归了。
无旋Treap还是很好写的,同时也兼备了普通Treap的特性,同时可以很好的支持区间反转、区间删除、区间询问等一系列区间问题,而这些区间操作是带有旋转的普通Treap力不能及的功能,所以我们也要好好记住无旋Treap。碰上区间操作,请考虑线段树和无旋Treap!

放在最后的话

或许,就这么多吧。。。
或许,未完待续。。。


  1. 根节点的值永远小于子节点的值 ↩︎

Treap平衡树学习笔记相关推荐

  1. 平衡树学习笔记之 fhq Treap

    平衡树学习笔记 1:fhq Treap(非旋 Treap) 正文开始前首先 %%% fhq 大佬. 众所周知,平衡树是一种 非常猥琐 码量堪忧的数据结构. 他的祖先是一种叫做二叉搜索树 ( B S T ...

  2. C/C++学习笔记:算法知识之平衡树学习笔记,收藏一波吧!

    平衡树存储: size就是节点的个数. value是节点代表的权值. 权值相同的两个节点被视为一个,num记录折叠数量. rand是随机数,用来维护平衡树. son就是两个儿子. 平衡树size更新: ...

  3. 平衡树 - FHQ 学习笔记

    平衡树 - FHQ 学习笔记 主要参考万万没想到 的 FHQ-Treap学习笔记. 本片文章的姊妹篇:平衡树 - Splay 学习笔记. 感觉完全不会平衡树,又重新学习了一遍 FHQ,一口气把常见套路 ...

  4. pbds库学习笔记(优先队列、平衡树、哈希表)

    目录 pbds库学习笔记(优先队列.平衡树.哈希表) 前言 概述 priority_queue优先队列 概述 参数 堆的基本操作的函数 对比STL新增函数 modify修改 Dijkstra最短路径演 ...

  5. 平衡树(splay)学习笔记(详细,从入门到精(bao)通(ling))(持续更新)

    前言 在前几天军训站军姿的时候胡思乱想,突然明白了splay的本质 KMP学习笔记后又一篇字数上万的题解- 前置技能--二叉搜索树 首先来看一个最简单的问题: 你需要维护一个数据结构,资磁这些操作: ...

  6. [学习笔记]可持久化数据结构——数组、并查集、平衡树、Trie树

    可持久化:支持查询历史版本和在历史版本上修改 可持久化数组 主席树做即可. [模板]可持久化数组(可持久化线段树/平衡树) 可持久化并查集 可持久化并查集 主席树做即可. 要按秩合并.(路径压缩每次建 ...

  7. 【算法竞赛学习笔记】pb_ds-超好懂的数据结构

    title : pb_ds date : 2021-8-21 tags : ACM,数据结构 author : Linno 简介 pb_ds库全称Policy-Based Data Structure ...

  8. MySQL服务器学习笔记!(二) ——数据库各项操作

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://foreveryan.blog.51cto.com/3508502/657640 ...

  9. l2-004 这是二叉搜索树吗?_算法学习笔记(45): 二叉搜索树

    二叉搜索树(Binary Search Tree, BST)是一种常用的数据结构,在理想情况下,它可以以 的复杂度完成一系列修改和查询,包括: 插入一个数 删除一个数 查询某数的排名(排名定义为比该数 ...

最新文章

  1. 新浪云python示例_在新浪云上部署Django应用程序
  2. 隐马尔可夫模型(Hidden Markov Model,HMM)是什么?隐马尔可夫模型(Hidden Markov Model,HMM)的三个基本问题又是什么?
  3. 【Android SOAP】基于第三方开源项目ksoap-android
  4. 有限状态机时代终结的10大理由
  5. 多线程编程3 - NSOperationQueue
  6. 设计模式之_动态代理_01
  7. [小技巧][JAVA][转换]List, Integer[], int[]的相互转换
  8. 2017年4月5号课堂笔记
  9. 网站静态页面克隆 | 学习笔记
  10. Ubuntu 16.04下设置开机时自动开启NumLock
  11. Qt系列文章之 右键菜单QMenu(下)
  12. 统计push点击次数的shell脚本最初版本1
  13. WinPE环境下WinNTSetup使用说明(WIM_ESD系统如何安装)
  14. 【武器系统】【2008.06】海军巡航导弹的制导与控制
  15. 提高Web页面渲染速度的7个技巧
  16. 王爽汇编语言 实验5
  17. 2022.01.26翻译Watermelon
  18. Java单例模式中的线程安全问题
  19. 如何打造出让人欲罢不能的“爆款”产品,这5个秘籍你收好!
  20. webrtc的DEMO环境搭建

热门文章

  1. Axure教程:一个通用的app注册/登录页
  2. Python实现Excel与Word文件中表格数据的导入导出
  3. 网络访问之HttpURLConnection
  4. linux下的线程编译,Linux下的多线程下载工具Axel编译安装
  5. 讲一讲移动端跨平台技术的演进之路
  6. python使用cxfreeze将脚本文件打包为可执行程序的方法
  7. 经典查找算法 - 顺序查找法
  8. Shell脚本基础2-变量和备注
  9. opencv saturate_cast使用详解
  10. 58到家立体监控平台:三大方面九个维度,架构流程及细节解析