【数据结构】 Treap1: 插入,删除,查找,旋转

Treap 详解1

今天我刚刚学习了一个新的数据结构 - Treap!

Part 1.1 \text{Part 1.1} Part 1.1 Treap的作用

Treap 分为旋转 Treap 和无旋 Treap, 本文主要讲解旋转 Treap.

旋转 Treap 支持以下操作:

  • 插入一个数 , 时间复杂度为 O ( h ) O(h) O(h).
  • 删除一个数 , 时间复杂度为 O ( h ) O(h) O(h).
  • 查找一个数 , 时间复杂度为 O ( h ) O(h) O(h).
  • 查询某个数字的排名 , 时间复杂度为 O ( h ) O(h) O(h).
  • 根据排名查询数字 , 时间复杂度为 O ( h ) O(h) O(h). (本文不讨论这个, 因为作者还不会)

其中 h h h 指的是树高, 在期望情况下, h = log ⁡ N h = \log N h=logN, N N N 是节点的数量

但是实际上不是, 作者实测后, 在 N = 10 6 N=10^6 N=106 时, 树高平均是 51 51 51. 而 log ⁡ 2 ( 10 6 ) = 19.1316 \log_2(10^6)=19.1316 log2(106)=19.1316

不过还是不错的, 因为普通的二叉查找树的树高是 N \sqrt{N} N 级别的, 而 N = 10 6 N=10^6 N=106 时平均树高是 1223 1223 1223(实测取平均值).

但是 set \text{set} set 的效率是我的 1.3 1.3 1.3 倍.(哭死)

Part 1.2 \text{Part 1.2} Part 1.2 Treap的节点定义

Treap 叫做树堆.

将 Tree 和 Heap 合成就是 Treap.

所以 Treap 的每一个节点都包含两个数字:

  • 元素的 ( value \text{value} value)
  • 元素作为堆的时候的优先级 ( priority \text{priority} priority)

所以 Treap 是包含二叉搜索树和堆两种性质的树.

下面给出 Treap 的节点定义程序.

cpp 复制代码
struct Node_t {
	Node_t* rtSon[2];		// 两个儿子节点
	int size, same_count, val, rank;		// 子树大小, 相同个数, 值, 优先级
};

与堆不同, 二叉搜索树的堆的优先级是随机给出的.

因为 C 库的 rand 实在太慢, 下面给出一个 rand.

cpp 复制代码
int _rand() {
	static unsigned long long seed = 114514;        // 表示静态变量, 函数运行后值会保留下来, 不是原来的值
	seed ^= (seed << 13);
	seed ^= (seed >> 7);
	seed ^= ~(seed << 31);  // 一通乱搞
	int ret = seed;
	return ret < 0 ? -ret : ret;        // 如果是负数就变成正数
}

下面给出完整结构体, 请读者自己推敲.

cpp 复制代码
int _rand() {
	static unsigned long long seed = 114514;
	seed ^= (seed << 13);
	seed ^= (seed >> 7);
	seed ^= ~(seed << 31);
	int ret = seed;
	return ret < 0 ? -ret : ret;
}
struct Node_t {
	Node_t* rtSon[2];		// 两个儿子节点0=左1=右
	int size, same_count, val, rank;		// 子树大小, 这个值的相同个数, 值, 优先级
	Node_t(int __v) : size(1), same_count(1), val(__v), rank(_rand()) {rtSon[0] = rtSon[1] = NULL;}
	void maintain() {
        // 这是一个维护函数, 维护这个子树的大小
		size = same_count;
		if (rtSon[0] != NULL) size += rtSon[0]->size;		// 判断有没有左节点
		if (rtSon[1] != NULL) size += rtSon[1]->size;		// 判断有没有右节点
	}
};

Part 1.3 \text{Part 1.3} Part 1.3 Treap的旋转

这个旋转是一切操作的基础, 非常之重要.

本质上就是让这个节点为根, 然后继续满足二叉搜索树的性质.

下面给出代码.

cpp 复制代码
// 旋转节点 dir=0左旋 dir=1右旋
void rotate(Node_t* &root, int dir) {
	Node_t* NewRoot = root->rtSon[dir ^ 1];		// 新的根节点
	root->rtSon[dir ^ 1] = NewRoot->rtSon[dir];
	NewRoot->rtSon[dir] = root;					// 覆盖根节点
	root->maintain(), NewRoot->maintain();		// 维护节点
	root = NewRoot;
}

下面将进行旋转的模拟

复制代码
    A                               
   / \
  B   C  
     / \
    D   E

首先考虑左旋(dir=0), 其实就是将节点 C 作为新的子树的根 (当前的根为 A).

  • 节点 C 就是节点 A 的右节点(rtSon[1]),所以 dir ^ 1 = 1
  • 然后将 C 这个位置改为节点 D. (代码中的 root->rtSon[dir ^ 1] = NewRoot->rtSon[dir];)
  • 再将节点 C 的左节点 D 改为节点 A
  • 再覆盖掉根节点

现在,这个树变成了这个样子

复制代码
    C                               
   / \
  A   E
 / \
B   D

形式化的讲:

  • 左旋这个节点其实是将这个节点的右节点和根节点交换, 并且交换之后满足二叉搜索树的性质.
  • 右旋同理, 就是将这个节点的左节点和根节点交换.

Part 1.4 \text{Part 1.4} Part 1.4 Treap的插入

这里我们设 r o o t root root 表示当前的节点, v a l val val 表示插入的数字.

  • 情况1: 当前节点为空节点: 那么我们考虑新建一个节点, 这个节点的值就是 v a l val val. 然后插入成功
  • 情况2: 当前 r o o t . v a l = = v a l root.val==val root.val==val, 那么我们考虑增加 root.same_count. 插入成功
  • 情况3: 当前节点小于 v a l val val. 因为左子树 > 右子树, 我们就递归去左子树里面插入.
  • 情况4: 当前节点大于 v a l val val. 同理, 我们递归到右子树里面

情况3和情况4可以使用一个通用的方法, 以减少代码量和出错的可能. 详见代码.

但是还有一种情况, 插入的节点违反了堆性质.

然后我们考虑堆的向上调整, 其实就是旋转上调这个节点, 可以自行的模拟.(详解上面旋转-形式化的说)

cpp 复制代码
// 插入节点
void insert(Node_t* &root, int val) {
	if (root == NULL) root = new Node_t(val);	
	else if (root->val == val) root->same_count++;
	else {
		int dir = val < root->val;		// 位置
		insert(root->rtSon[dir], val);	// 插入节点
		if (root->rank > root->rtSon[dir]->rank) rotate(root, dir ^ 1); // 这里可以参加 "形式化的说"
	}
	root->maintain();					// 维护节点
}

Part 1.5 \text{Part 1.5} Part 1.5 Treap的删除

删除应该也不难.

发现也可以像删除分成各种情况, 然后一一进行.

代码其实和插入很相似, 这里给出代码, 思考的时间留给读者.

cpp 复制代码
// 删除节点
void erase(Node_t* &root, int val) {
	
	// 值相等
	if (root->val == val) {
		if (root->same_count > 1) {root->same_count--; return ;}		// 相同的节点直接减去 1 即可
		if (root->rtSon[0] == NULL && root->rtSon[1] == NULL) {delete root; return ;}		// 是叶子节点, 直接删去
		if (root->rtSon[0] != NULL && root->rtSon[1] != NULL) {
			// 均不为空的情况, 查看优先级
			int dir = (root->rtSon[0]->rank < root->rtSon[1]->rank) ? 0 : 1;
			rotate(root, dir);		// 将这个节点向上移动
			erase(root->rtSon[dir], val);	// 继续删除根节点
			return ;
		}
		Node_t* tmp = root;
		if (root->rtSon[0] != NULL) root = root->rtSon[0];		// 只有一个节点为空, 可以直接交换
		else root = root->rtSon[1];
		
		delete tmp;		// 删除节点
	}
	else erase(root->rtSon[root->val < val ? 0 : 1], val);
	if (root != NULL) root->maintain();		// 维护节点大小
}

总结

这个数据结构并没有想象中的那么难, 希望读者可以学会这个东西.

然后这里就讲这么多, 代码有错误或说明有问题可以在评论区反馈给作者.

查询名次下次再写.

这里给出一个学习链接 https://oi-wiki.org/ds/treap/.

相关推荐
core5122 小时前
ResNet 残差连接:通往深层网络的“高速公路”
人工智能·算法·resnet
聆风吟º2 小时前
【数据结构手札】顺序表实战指南(五):查找 | 任意位置增删
数据结构·顺序表·查找·任意位置增删
曾几何时`2 小时前
滑动定窗口(十四)2831. 找出最长等值子数组
数据结构·算法
报错小能手2 小时前
数据结构 哈希基础 哈希函数 哈希冲突及解决
数据结构·哈希算法·散列表
柯慕灵2 小时前
轻量推荐算法框架 Torch-rechub——基于PyTorch
pytorch·算法·推荐算法
源代码•宸2 小时前
goframe框架签到系统项目开发(用户认证中间件、实现Refresh-token接口)
数据库·经验分享·后端·算法·中间件·跨域·refreshtoken
YGGP2 小时前
【Golang】LeetCode 300. 最长递增子序列
算法·leetcode
隐语SecretFlow2 小时前
隐语SML0.1.0版本发布!SPU开源机器学习Python算法库
python·算法·机器学习
zdd567892 小时前
GIN索引原理
运维·算法·postgresql