每日一题之 hiho1325 Treap

描述
小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似。

小Hi:你说的是哪两个啊?

小Ho:就是二叉排序树和堆啊,你看这两种数据结构都是构造了一个二叉树,一个节点有一个父亲和两个儿子。 如果用1…n的数组来存储的话,对于二叉树上的一个编号为k的节点,其父亲节点刚好是k/2。并且它的两个儿子节点分别为k2和k2+1,计算起来非常方便呢。

小Hi:没错,但是小Hi你知道有一种办法可以把堆和二叉搜索树合并起来,成为一个新的数据结构么?

小Ho:这我倒没想过。不过二叉搜索树满足左子树<根节点<右子树,而堆是满足根节点小于等于(或大于等于)左右儿子。这两种性质是冲突的啊?

小Hi:恩,你说的没错,这两种性质的确是冲突的。

小Ho:那你说的合并是怎么做到的?

小Hi:当然有办法了,其实它是这样的…

提示:Tree+Heap?

输入
第1行:1个正整数n,表示操作数量,10≤n≤100,000

第2…n+1行:每行1个字母c和1个整数k:

若c为’I’,表示插入一个数字k到树中,-1,000,000,000≤k≤1,000,000,000

若c为’Q’,表示询问树中不超过k的最大数字

输出
若干行:每行1个整数,表示针对询问的回答,保证一定有合法的解

样例输入
5
I 3
I 2
Q 3
I 5
Q 4
样例输出
3
3

思路:

小Hi:由于是二叉搜索树和堆合并构成的新数据结构,所以它的名字取了Tree和Heap各一半,叫做Treap。在有的教材中,把Treap翻译成树堆。

正如你刚刚所说,堆和树的性质是冲突的。因此在Treap的数据结构中,并不是以单一的键值作为节点的数据域。

Treap每个节点的数据域包含2个值,key和weight。

key值,和原来的二叉搜索树一样,满足左子树<根节点<右子树。

weight值,随机产生。在Treap中weight值满足堆的性质,根节点的weight值小于等于(或大于等于)左右儿子节点。

比如下图就是一个示例的Treap:

1.jpg

小Ho:感觉上就是在原来二叉搜索树上增加了一个weight值。

小Hi:简单来理解的话,就是这样。

因此Treap的大部分操作都和二叉搜索树是一样的,唯一区别在于每次插入一个节点后,需要对树的结构进行调整。

因为每一个节点的weight值不一样,当我们按照key值插入一个节点后,这个节点有可能不满足weight值的要求。

对于如何调整,首先我们来看一个最简单的例子:

在这里插入图片描述

如图所示的一个Treap有三个节点,其中根的右儿子节点是新插入的。

假设我们一开始想要让Treap满足小根堆的性质,即weight值越小越在堆顶。

那么我们需要在不改变key值顺序的情况下,对节点进行变形,使得weight值满足性质。

这一步骤被称为旋转,对于例子,其旋转之后的形态为:

在这里插入图片描述

根据旋转的方向不同,旋转分为两种:左旋和右旋。

在例子中是将右儿子节点旋转至根,所以称为左旋。反之将左儿子节点旋转至根,称为右旋。

那么这个旋转具体的过程,我们可以对应旋转前后的图来分析。首先是左旋操作:

在这里插入图片描述
它的过程有如下几步:

  1. 获取根节点A的右儿子节点B
  2. 将节点B的父亲节点信息更新为f,并更新节点f的子节点信息为B
  3. 将节点A的右儿子信息更新为节点B的左儿子D,同时将节点D的父亲节点信息更新为A
  4. 将节点B的左儿子信息更改为节点A,同时将节点A的父亲节点信息更改为B
    该过程的伪代码如下:
left-rotate(a):
	b = a.right
	b.father = a.father
	If (a.father.left == a) Then
		a.father.left = b
	Else
		a.father.right = b
	End If
	a.right = b.left
	b.left.father = a
	b.left = a
	a.father = b
		

然后是右旋操作:

在这里插入图片描述

其过程是左旋操作的镜像:

  1. 获取根节点A的左儿子节点B
  2. 将节点B的父亲节点信息更新为f,并更新节点f的子节点信息为B
  3. 将节点A的左儿子信息更新为节点B的右儿子D,同时将节点D的父亲节点信息更新为A
  4. 将节点B的右儿子信息更改为节点A,同时将节点A的父亲节点信息更改为B
    该过程的伪代码如下:
right-rotate(a):
	b = a.left
	b.father = a.father
	If (a.father.left == a) Then
		a.father.left = b
	Else
		a.father.right = b
	End If
	a.left = b.right
	b.right.father = a
	b.right = a
	a.father = b

小Ho:旋转的操作我是理解了,但是要怎么运用呢?

小Hi:只要将节点插入Treap以后,再不断的旋转当前节点直到weight满足堆的性质。

首先我们从插入操作来看,这里我们让insert完成后返回新加入的节点:

insert(node, key):
	If (key < node.key) Then
		If (node.left is empty) Then
			node.left = {key: key, weight: random()}
			Return node.left
		Else 
			Return insert(node.left)
		End If
	Else
		If (node.right is empty) Then
			node.right = {key: key, weight: random()}
			Return node.right
		Else 
			Return insert(node.right)
		End If
	End If

完成插入操作后,我们获得了新加入的节点,然后迭代的进行旋转(这里假设采用小根堆):

rotate(node):
	While (node.father is not empty)
		fa = node.father
		If (node.weight < fa.weight) Then
			If (node == fa.left) Then
				right-rotate(fa)
				// 这里的参数是父节点
				// 在该处fa对应了旋转中的节点a, node对应了节点b
			Else
				left-rotate(fa)
			End If
		Else
			Break
		End If
	End While

需要注意的是,对于我这种插入的写法,需要手动在Treap中插入第一个元素:

main():
	root = NULL
	For i = 1 .. n
		input x
		If (root is empty) 
			root = {key: x, weight: random()}
		Else 
			rotate(insert(root, x))
		End If
	End For

另外还有一点,相比较于普通的二叉搜索树,Treap删除节点的操作也有一定的区别。

同样需要根据删除节点的孩子数量来进行处理:

  1. 没有孩子节点,则当前结点为叶子节点,直接删去即可。

  2. 有一个孩子节点,和普通二叉搜索树相同,让孩子节点代替当前节点。

  3. 有两个孩子节点,利用旋转,将weight值小(或大)的子节点旋转到根上,将待删除节点向下旋转。反复操作直到待删除节点只有0个或1个子节点。

其伪代码为:

delete(key):
	node = find(key) 
	// 这里的find操作和普通的二叉搜索树相同
	While (node's child is full)
		// 从儿子节点中找到weight值小的一个旋转至根
		child = node.left // 先初始为左节点较小
				
		If (child.weight > node.right.weight)
			// 比较左右节点
			child = node.right
		End If
		
		// 进行旋转
		If (child == node.left) Then
			right-rotate(node)
		Else
			left-rotate(node)
		End If
	End While
	
	// 此时当前结点只有一个或零个儿子
	fa = node.fa
	If (node.left is not empty) Then
		// 当前结点只有一个左儿子
		node.left.fa = fa		
		If (node == fa.left) Then
			fa.left = node.left
		Else
			fa.right = node.left
		End If
	Elseif (node.right is not empty) Then
		// 当前结点只有一个右儿子
		node.right.fa = fa		
		If (node == fa.left) Then
			fa.left = node.right
		Else
			fa.right = node.right
		End If
	Else
		// 已经旋转到叶子节点,删去当前结点
		If (node == fa.left) Then
			fa.left = NULL
		Else
			fa.right = NULL
		End If
	End If

小Ho:我明白了,但是总的来说Treap和普通的二叉搜索树实现的功能是一样的,为什么要花费额外的时间去做旋转操作呢?

小Hi:对于一般的二叉搜索树,在某些特殊情况下根据输入数据来建树有可能退化为一条链,比如一个依次增大的数列。

而如果一棵二叉排序树的节点是按照随机顺序插入,得到的二叉排序树大多数情况下是平衡的,其期望高度是O(logn)。

因此Treap利用weight值作为随机因子来调整二叉树的形状,使得在大部分情况下比直接通过数据建立的二叉树要平衡。

每一次查找的期望复杂度也会降低,总体的速度也就得到了提高。

#include <bits/stdc++.h>

using namespace std;

const int mod = 1e9+7;

struct Treap {
	int key;
	int weight;
	Treap* left;
	Treap* right;
	Treap* father;
	Treap(int k, int w) {
		key = k;
		weight = w;
		left = nullptr;
		right = nullptr;
		father = nullptr;
	}
};

void leftRotate(Treap* a) {
	Treap* b = a->right;
	b->father = a->father;
	if (a->father->left == a) {
		a->father->left = b;
	}
	else {
		a->father->right = b;
	}
	a->right = b->left;
	b->left->father = a;
	b->left = a;
	a->father = b;
}

void rightRotate(Treap* a) {
	Treap* b = a->left;
	b->father = a->father;
	if (a->father->left == a) {
		a->father->left = b;
	}
	else {
		a->father->right = b;
	}
	a->left = b->right;
	b->right->father = a;
	b->right = a;
	a->father = b;
}

Treap* Insert(Treap* node, int key) {
	if (key < node->key) {
		if (node->left == nullptr) {
			Treap* tmp = new Treap(key,rand()%mod);
			node->left = tmp;
			return node->left;
		}
		else {
			return Insert(node->left, key);
		}
	}
	else {
		if (node->right == nullptr) {
			Treap* tmp = new Treap(key,rand()%mod);
			node->right = tmp;
			return node->right;
		}
		else {
			return Insert(node->right, key);
		}
	}
}

void rotate(Treap* node) {
	while(node->father != nullptr) {
		Treap* fa = node->father;
		if (node->weight < fa->weight) {
			if (node == fa->left) {
				rightRotate(fa);
			}
			else 
				leftRotate(fa);
		}
		else 
			break;
	}
}

void Search(Treap* cur, int x, int &res) {
	if (cur == nullptr) return;
	if (cur->key == x) {
		res = x;
		return;
	}
	if (cur->key > x) {
		Search(cur->left, x, res);
	}
	else {
		res = cur->key;
		Search(cur->right, x, res);
	}
}

int main() {
	int n;
	cin >> n;
	char op;
	int x;
	int res = 0;
	Treap* root = nullptr;
	while(n--) {
		cin >> op >> x;
		if (op == 'I') {
			if (root == nullptr) {
				root = new Treap(x, rand()%mod);
			}
			else 
				rotate(Insert(root, x));
		}
		else {
			Search(root, x, res);
			cout << res << endl;
		}

	}

	return 0;
}
简单解法

其实还有一种简单的解法,那就是直接利用stl封装好的容器和算法。利用set和upper_bound()

#include<bits/stdc++.h>

using namespace std;


int main()
{
    int n, x;
    char op;
    set<int> s;
    set<int>::iterator it;
    cin >> n;
    while(n--) {
        cin >> op >> x; 
        if(op=='I') {
            s.insert(x);
        }
        else {
            it=s.upper_bound(x);
            cout << *--it << endl;
        }
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值