BST-Treap学习笔记

        树堆(tree+heap)Treap是一种平衡二叉搜索树。

        在常规的二叉树中,树的形状被输入的顺序所影响,最差的情况下,树会退化成一个链表;而最好的情况下树应该是平衡的(秩最低),Treap的解决方法是:给每一个结点随机地赋予优先级,然后按优先级而非键值插入结点。在后文中,我们将会详细地解释优先级、键值在Treap中起到的作用以及Treap的实现模板。

优先级和键值-Treap的两个维度

        二叉树是一种二维的数据结构,它具有宽度和高度。在常规的树中,键值(key)决定的了树的宽度梯度(左高右低),而输入的时间先后决定了树的高度梯度(上先下后),也就是越晚输入的数在树的越下面,而键值越大的数在树的越右边。

         那么在Treap中有什么不同呢?我们为了不让输入顺序影响我们树的形状,因此引入了优先级(rank)去代替输入顺序,来决定我们树的高度梯度。这个优先级往往是随机的,因此尽管往往不会达到平衡的最优解,但期望的复杂度是O(log2n)的。

        清楚了优先级和键值的作用地位,你几乎已经了解了Treap树的一切。你会发现对于键值来说,这是一棵二叉搜索树,对于优先级来说,这是一个堆(根结点的优先级总是最大的)。

        最后在我们开始实现它之前,再强调一下:Treap的两个维度是互相垂直的,互不影响,因此左右儿子不存在优先级的绝对关系,而它们的优先级都比它们的根节点低;同样的,优先级关系确定的结点也无法确定键值的关系。

Treap结点

        一个Treap结点应该有以下接口:

(1)键值 key

(2)优先级 rank

(3)当前结点的子树的结点总数(用于名次树) size

(4)左二子son[0] , 右儿子son[1]

(5)重载<用于比较两个结点优先级的大小(不一定需要)

(6)一个比较两个结点键值大小且返回0,1值的函数(用于对应左右儿子)

(7)一个用于更新结点size的函数(需要3时则需要)

struct Node{
	int size;			//当前节点的子树节点总数,用于名次树 
	int rank;			//优先级 
	int key;			//键值 
	Node* son[2];		//0是左儿子,1是右儿子 
	bool operator<(const Node& a)const{return rank<a.rank;}		//重载< 
	int cmp(int x)const{	//const代表函数是只读的 
		if(x==key)	return -1;	//Treap树不应存在相同键值的节点 
		return x<key?0:1;		//x本身是一个键值,判断x和当前节点的优先级大小 
	}
	void update(){		//更新size 
		size = 1;
		if(son[0]!=NULL) size += son[0]->size;
		if(son[1]!=NULL) size += son[1]->size;
	} 
}; 

Treap的动态调整-左旋和右旋

        我们可以将树所有的结点的信息都储存完毕了,再按键值和优先级插入它们,但这会浪费大量栈的空间,为了更好,我们先根据键值朴素地插入新结点,这个时候的树只是普通的二叉树,然后通过动态调整的手段根据新的结点的优先级调整它的位置,覆盖掉输入顺序的影响,使其称为Treap树——Treap的旋转

        Treap的旋转是一个比较抽象的概念,左和右是根据Treap在旋转后平面上的变化命名的。旋转之所以比较抽象和复杂,是因为它的变化同时影响了key和rank两个维度,我们不得不在遵守key梯度规则的同时修正树使得其满足rank梯度规则。无论如何,我们可以这样记住旋转的过程:

        我们将要进行旋转的结点称为主(图中的k),主结点原本的根结点称为根(图中的o);

左旋:根的右变成主的左,主的左变成根,根变成主;

右旋:根的左变成主的右,主的右变成根,根变成主。

void rotate(Node* &o, int d){		//d=0左旋,d=1右旋 
	Node *k = o->son[d^1];		//^异或运算符,同0异1,此处等于1-d 
	o->son[d^1] = k->son[d];    //根的右/左变成主的左/右
	k->son[d] = o;              //主的左/右变成根
	o->update();		//更新size
	k->update();		
	o = k;		 		 //根变成主
}

 Treap结点的插入

void insert(Node* &o, int x){		//把x插入到树 
	if(o==NULL){
		o = new Node();			()调用默认构造函数进行初始化
		o->son[0] = o->son[1] = NULL;
		o->rank = rand();		//随机赋予优先级
		o->key = x;
		o->size = 1; 
	}
	else{
		int d = o->cmp(x);	//判断x和o的键值大小 
		insert(o->son[d],x);
		o->update();
		if(o<o->son[d])		//判断o和o被修改的子树的优先级是否违法 
			rotate(o,d^1);
	} 
}

Treap结点的删除

        对于Treap结点的删除有两种情况:

(1)如果结点是叶子结点(没有子结点),则直接删除。

(2)结点有子结点,判断子节点哪个优先级高,往反方向(优先级低的方向)旋转,将优先级高的子结点转上来,保证优先级的顺序不变。

        代码实现步骤:

(1)查找结点位置。

(2)按情况删除结点。

(3)回溯,更新size


void Delete(node* &root,int x){
	int d=root->cmp(x);        //判断键值
	if(d==-1){               //键值一样,找到了
		node* u=root;
		if(root->son[0]!=NULL&&root->son[1]!=NULL){
			int d2=(root->son[0]>root->son[1]?1:0);    //判断优先级大小
			rotate(root,d2);                //向反方向旋转
			remove(root->son[d2],x);
		}
		else{
			if(root->son[0]==NULL)
				root=root->son[1];
		    else  
                root=root->son[0];
			delete u;
		}
	 } 
	 else remove(root->son[d],x);
	 if(root!=NULL)
	 	o->update();    //在回溯中更新size
}

Treap结点名次的查询

        在treap树中存在名次,所谓名次即这个结点是第几大的。名次是一个键值维度的量,优先级大小与结点名次无关。

       让我们来解释一下size、名次和名次查询的逻辑。 

        在下面这棵树中,我们将size(由该结点作为根节点的树的结点总数)标注在结点上:

        随后,我们分别把这棵树的键值大小和优先级进行标注,首先把键值大小标注在结点下面(第几大),随后把优先级用虚线区分: 

        仔细比对可以发现:size和优先级没有绝对关系,而右子树的size和名次是正相关的,左子树的size和名次是负相关的。 由于size没有左右的区别,而名次梯度是从右到左的,这是size和名次的左右相反性。

         先单纯考虑简单的情况:如果我们要找的结点在整棵树的右边,也就是寻找的路径没有左转,一直在往右子树深入,则显然:右儿子的size+1等于根节点的名次

        当然,这个结论在路径出现左转的时候由于名次的左右相反性出现错误,为了修正这个错误,当每次路径左转时,其右儿子的(size+1)*2才是真实的名次。 

        接下来放出模板函数,如果觉得理解思路比较困难,可以用纸笔多模拟一下搜索的过程。

 找到元素k的名次:

int Find(Node* o, int k){		//返回元素k的名次 
	if(o==NULL) 	return -1;	//树为空,没找到 
	int d = o->cmp(k);		//判断k和o的键值大小 
	if(d==-1) 		//k和o的键值一样
		return o->son[1]==NULL?1:o->son[1]->size+1;		//返回名次
	else if(d==1)	return Find(o->son[d],k);		//k的键值大,去右子树找 
	else{		//k的键值小 
		int tmp = Find(o->son[d],k);	//去左子树找	
		if(tmp == -1)	return -1;		//树为空,没找到 
		else							//找到了 
			return o->son[1] == NULL? tmp+1 : tmp+1+o->son[1]->size; 
            //左右名次相反性,回溯中恢复正确名次 
	} 
} 

找到名次为k的元素: 

int kth(Node* o,int k){		//返回第k大的数 
	if(o==NULL||k<=0||k>o->size) 	return -1;	//树是空的、k过小、过大
	int  s = o->son[1]==NULL?0:o->son[1]->size;	//s是右儿子的size,s+1是o的名次 
	if(k==s+1)	return o->key;		//说明k和o的名次一样,找到了
	else if(k<=s)	return kth(o->son[1],k);//k比o的名次小,先去它的右子树找 
	else return kth(o->son[0],k-s-1); 	//k比o的名次大,去它的左子树找,同时k=k-s-1 
}//对于右子树而言,size和名次是等价的,对于左子树而言,名次=k-s-1(名次的左右相反性) 

Treap的合并

        Treap的合并思路比较暴力,时间复杂度是O(nlogn),在小数据范围内还行。逻辑就是先找到根节点优先级较大的那个树(假设为x),再将另一棵树(y)的结点一个个拆散插入到x中,所以实际上是Delete和Insert函数的合成:


 void Merge(node* &x,node* &y)    //x根结点的优先级更高
 {
 	if(y->son[0]!=NULL) Merge(x,y->son[0]);
 	if(y->son[1]!=NULL) Merge(x,y->son[1]);
 	insert (x,y->key);    //找到y的叶子结点,插入进x
 	delete y;
 	y=NULL;     //在y中删除这个叶子结点,回溯
 }

Treap的一些补充操作

        基本的Treap操作已经写完了,作为数据结构的笔记,老规矩在最后放出Treap的一些补充操作(也许会更新……)

求x的前驱(小于x的最大数)

int Pre(Node* &root,int x){    //求x前驱(小于x的最大的数)
    if(root==NULL)    //若为空
        return INT_MIN;    //返回极小值
    if(root->key>=x)    //若当前点点权大于等于x
        return Pre(root->son[0],x);    //向左子树搜索
    return max(root->key,Pre(root->son[1],x));
    //否则返回当前点点权和向其右子树中返回值(若右子树为空则会直接返回极小值)
}

 求x的后继(大于x的最小数)

int Next(Node* &root,int x){    //求x后继(大于x的最小的数)
    if(root==NULL)    //若为空
        return INT_MAX;    //返回极大值
    if(root->key<=x)    //若当前点点权小于等于x
        return Next(root->son[1],x);    //向右子树搜索
    return min(root->key,Next(root->son[0],x));
    //否则返回当前点点权和向其左子树中返回值(若左子树为空则会直接返回极大值)
}

题目参考

        POJ1442 Black Box

        HDU4584 Shaolin

        HDU3726 Graph and Queries

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值