zoukankan      html  css  js  c++  java
  • 指针实现 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
    ",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

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

  • 相关阅读:
    Java Native Method
    SQL语句优化
    Ibatis的环境搭建以及遇到的问题解决
    Java 构建器
    SpringMVC自定义视图 Excel视图和PDF视图
    java 枚举的常见使用方法
    mysql 根据某些字段之和排序
    MFC The Screen Flickers When The Image Zoomed
    How To Debug Qmake Pro File
    Gcc And MakeFile Level1
  • 原文地址:https://www.cnblogs.com/createsj/p/treap.html
Copyright © 2011-2022 走看看