• 无旋Treap——FHQ Treap


    基本介绍

     我们知道,有旋 T r e a p Treap Treap通过旋转操作来维护树的平衡。但是实际上也有不用旋转就能维护树的平衡的 T r e a p Treap Treap。它就是 F H Q    T r e a p FHQ\;Treap FHQTreap。一个很牛逼的东西。

    F H Q    T r e a p FHQ\;Treap FHQTreap又称无旋 T r e a p Treap Treap,由范浩强发明。它不再使用旋转操作,改用分裂 ( s p l i t ) (split) (split)合并 ( m e r g e ) (merge) (merge)两个操作维护树的平衡。

    前置基本知识:Treap的原理

     通俗的将: T r e a p = T r e e + H e a p Treap=Tree+Heap Treap=Tree+Heap,即这是一棵具有树的性质又具有堆的性质的东西。
     平衡树上的每个节点放两个东西:树的值 v a l val val和节点随机的堆值 k e y key key。我们对于 v a l val val值,维护查找树的性质,对于 k e y key key值,我们维护堆的性质。

     我们又知道,普通的 B S T BST BST在插入有序序列的情况下树会退化成一条链,从而导致复杂度直接从 O ( log ⁡ n ) O(\log n) O(logn)上升到 O ( n ) O(n) O(n)。但是 T r e a p Treap Treap就不会(异或说很难,下文有解释)发生这种情况。堆的随机值等价于随机打乱有序序列插入的顺序。

     例如,我们要在 T r e a p Treap Treap中插入一个有序序列 1    2    3    4    5    6 1\;2\;3\;4\;5\;6 123456,如下图所示:
    在这里插入图片描述
     上图中浅绿色部分就是堆的随机值,原本插入有序序列 1    2    3    4    5    6 1\;2\;3\;4\;5\;6 123456,现在等价于 4    2    5    1    3    6 4\;2\;5\;1\;3\;6 425136

    FHQ Treap的基本操作

     在这一部分,我主要张贴带有注释的代码,必要的时候附上几张图片并加以文字说明。

    结点信息

    struct FHQ{
    	int ls,rs;//左右儿子 
    	int val;//树的权值 
    	int key;//堆的随机值 
    	int size;//subtree的大小 
    }t[M];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    新建结点

    int newbuild(int v){//新建一个权值为v的结点 
    	t[++idx].val=v;
    	t[idx].key=rand();//取堆的随机值 
    	t[idx].size=1
    	return idx;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    向上更新

    inline void pushup(int p){
    	t[p].size=t[t[p].ls].size+t[t[p].rs].size+1; 
    } //跟线段树差不多 
    
    • 1
    • 2
    • 3

    分裂(split)

     分裂的方法就是按照给定值 v v v将一棵树分裂成两棵树,其中一棵树上的 v a l val val全部小于等于 v v v,另一棵树上的 v a l val val全部大于 v v v

     如果当前节点的 v a l ≤ v val \le v valv,那么当前节点的左子树和它自己都属于分裂后的左 T r e a p Treap Treap,但是在这个点的右子树中可能还存在一部分也满足 v a l ≤ v val \le v valv的,此时我们还需要递归处理右子树中满足条件的部分,把满足条件的部分作为当前节点的右子树。把 x x x指向左 T r e a p Treap Treap的根。(如下图所示)
    在这里插入图片描述
     同理,如果当前节点的 v a l > v val \gt v val>v,那么当前节点的右子树和它自己都属于分裂后的右 T r e a p Treap Treap,但是在这个点的左子树中可能还存在一部分也满足 v a l > v val \gt v val>v的,此时我们还需要递归处理左子树中满足条件的部分,把满足条件的部分作为当前节点的左子树。把 y y y指向右 T r e a p Treap Treap的根。

    void split(int p,int v,int &x,int &y){//传一个引用,确定左右儿子 
    	if(!p){x=y=0;return;}
    	if(t[p].val<=v){
    		x=p;
    		split(t[p].rs,v,t[p].rs,y);//右儿子中可能还有 
    	}
    	else{
    		y=p;
    		split(t[p].ls,v,x,t[p].ls);//左儿子中可能还有 
    	}
    	pushup(p); 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

     假设此时的 v v v 3 3 3 p p p最开始指向 6 6 6这个节点。那么我们通过自己模拟后可以发现原树被分类为了这个。(下图右)

    在这里插入图片描述

    合并

     合并其实可以看作是分裂的逆向操作

     合并函数的两个参数:左 T r e a p Treap Treap的根指针 x x x,右 T r e a p Treap Treap的根指针 y y y。必须满足 x x x中所有结点的 v a l val val值小于等于 y y y中所有结点的 v a l val val值。(已经在分裂的时候就已经满足)。

     因为两个 T r e a p Treap Treap已经有序,所以在合并时只需要考虑把哪科树放在上面,把哪棵树放在下面。也就是判断将哪棵树作为子树。根据堆的性质,我们将 k e y key key值小的放在上面。递归即可解决。

    int merge(int x,int y){
    	if(!x or !y)return x+y;
    	if(t[x].key<t[y].key){//把x放在上面 
    		t[x].rs=merge(t[x].rs,y);//x已经有左儿子了,那就作为右儿子 
    		pushup(x);return x;//回溯修改标记
    	}
    	else{//把y放在上面 
    		t[y].ls=merge(x,t[y].ls);//y已经有右儿子了,那就作为左儿子 
    		pushup(y);return y;
    	}
    }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    插入

     先分裂,再新建节点,再合并。

    void insert(int v){
    	int x,y,z;
    	split(root,v,x,y);//分裂,方便之后的合并 
    	z=newbuild(v);//新建一个点 
    	root=merge(merge(x,z),y);//先合并x对应子树与z节点,确保val的大小 
    } //更新root
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    删除

     删除也可以看作是插入的逆过程。
     两次分裂,第一次对整棵树按照当前的数的大小分裂,第二次对左 T r e a p Treap Treap按照当前数大小减去 1 1 1进行分裂,这样我们就可以得到 3 3 3个部分(如下图左下角与右上角),此时我们再合并y的左右儿子,相当于是删去了要删的这个数,因为它自此没有了原本的根节点的那个数。最后再按照大小顺序合并一遍即可。(图中删去 3 3 3
    在这里插入图片描述

    void del(int v){
    	int x,y,z;
    	split(root,v,x,z);
    	split(x,v-1,x,y);
    	y=merge(t[p].ls,t[p].rs);
    	root=merge(merge(x,y),z);
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    返回第k个节点

     分三种情况,第一种情况就是 k k k小于等于左子树的大小,那么直接在左子树中递归查找。第二种情况就是 k k k恰好等于左子树的大小加 1 1 1,那么当前节点就是要找的数,最后一种情况就只需要在右子树中查找,但是此时的排名要减去左子树的大小 + 1 +1 +1.。

    int get_k(int p,int k){
    	if(t[t[p].ls].size<=k)get_k(t[p].ls,k);
    	if(k==t[t[p].ls].size+1){
    		return p;
    	}
    	return get_k(t[p].rs,k-t[t[p].ls].size-1);
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    求前驱与求后继

     假设我们现在有这样一棵树,我们要找 5 5 5的前驱,很显然我们可以发现 5 5 5的前驱是 4 4 4,于是我们就可以按照 4 4 4来分裂,在左 T r e a p Treap Treap中找最后一个数即可。
    在这里插入图片描述
     找后继也是一样的,只不过要按照 v v v分裂,然后在右 T r e a p Treap Treap中找第一个就行了。(千万别忘了合并回去)

    void getpre(int v){//找前驱
    	int x,y;
    	split(root,v-1,x,y);
    	int p=get_k(x,t[x].size);
    	cout<<t[p].val<<'\n';
    	root=merge(x,y); 
    }
    
    void getnxt(int v){//找后继
    	int x,y;
    	split(root,v,x,y);
    	int p=get_k(y,1);
    	cout<<t[p].val<<'\n';
    	root=merge(x,y);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    按照排名找值

    void getvalbyrk(int root,int k){
    	int p=getrk(root,k);
    	cout<<t[p].val<<'\n';
    }
    
    • 1
    • 2
    • 3
    • 4

    按照值找排名

    void getrkbyval(int v){
    	int x,y;
    	split(root,v-1,x,y);
    	cout<<t[x].size+1<<'\n';
    	root=merge(x,y); 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 相关阅读:
    在 Visual Studio 中使用远程 MacOS 调试功能
    伦敦金走势技术指标的背离
    人工智能大模型专题报告:方兴未艾,并驱争先
    ProEasy机器人:运动+通讯相关说明
    vue2源码学习(1)
    .NET Core WebAPI 部署IIS后 Swagger.json 提示没有找到问题与解决方案
    03 OLED显示屏实现
    Unity读取写入Excel
    python 2018全国自学考试第5章 第35题,求出了完整数但是没有达到题目的要求。继续修改,今天又有了另一个收获 排名21297
    数据结构题目收录(二十二)
  • 原文地址:https://blog.csdn.net/Lucas_FC_/article/details/126585868