入门平衡树:\(treap\)

前言:

  • 如有任何错误和其他问题,请联系我

    • 微信/QQ同号:615863087

前置知识:

  • 二叉树基础知识,即简单的图论知识。

初识\(BST\):

  • \(BST\)是\((Binary\:\:Search\:\:Tree)\)的简写,中文名二叉搜索树。
  • 想要了解平衡树,我们就先要了解这样一个基础的数据结构: 二叉搜索树。
  • 所以接下来会先大篇幅讨论\(BST\)
  • 了解了\(BST\)后,\(Treap\)也就顺理成章了。
  • 二叉树有两类非常重要的性质:
    • 1:堆性质

      • 堆性质又分为大根堆性质和小根堆性质。小根堆的根节点小于左右孩子,且这是一个递归定义。对于大根堆则是大于。\(c++\)提供的\(priorioty\_queue\)就是一个大根堆。
    • 2:\(BST\)性质
      • 给定一棵二叉树,树上的每个节点都带有一个数值,成为节点的“关键吗”。对于树中任意一个节点满足\(BST\)性质,指:

        • 该节点的关键码不小于他左子树的任意节点的关键码。
        • 该节点的关键码不大于他右子树的任意节点的关键码。
  • 举个拓展的例子,笛卡尔树既满足堆性质,又满足\(BST\)性质。
    • 笛卡尔树并不常见,博主也只见过几次,也偶尔成功地用别的数据结构\(AC\)掉对应的题目,是下来后看题解才发现可以用笛卡尔树写。

    • 似乎可以用单调栈替代?希望了解的奆奆可以联系我或者评论区告诉我。

    • 这里给出一张笛卡尔树的图片帮助大家了解\(BST\)性质和堆性质,不多做介绍。

    • (图源:维基百科)

    • 笛卡尔树中的\(index\)满足\(BST\)性质,\(value\)满足堆性质。

      • 其中\(index\)是图中蓝色框出现的顺序,\(val\)是蓝色框出现的权值。
      • 很显然这棵树满足小根堆性质,也满足\(BST\)性质。
  • 那么我们接下来介绍的二叉搜索树就是一棵满足\(BST\)性质的二叉树,可以结合此图加深理解。

\(BST\)的相关操作:

\(BST\)的建立:
  • 为了避免数组越界同时减少特判,我们一般在\(BST\)中插入两个额外的节点,其中两个节点的权值其中一个为\(+INF\),另一个为\(-INF\)。
  • 不明白为什么能够减少特判可以先往下阅读。
const int INF = 0x3f3f3f3f;
int tot, root, n;
struct Treap //入门级平衡树
{
int l, r; //左右子节点在数组中的下标
int val; //节点权值
}a[maxn]; int New(int val)
{
a[++tot].val = val;
return tot;
} void Build()
{
New(-INF), New(INF);
root = 1; a[1].r = 2;
}
\(BST\)的检索:
  • 为了方便,我们假设树中没有相同权值的节点。
  • 在\(BST\)中检索关键码为\(val\)的节点。根据\(BST\)性质,我们可以:
    • 当\(val\)等于当前节点的关键码

      • 说明找到了这个节点
    • 当\(val\)小于当前节点的关键码
      • 若当前节点左子树为空,则说明不存在\(val\)。
      • 否则递归进入左子树。
    • 当\(val\)大于当前节点的关键码
      • 若当前节点右子树为空,则说明不存在\(val\)。
      • 否则递归进入右子树
int Get(int &p, int val)
{
if(p == 0)
{
p = New(val);
return;
}
if(val == a[p].val) return p;
return val < a[p].val ? Get(a[p].l, val) : Get(a[p].r, val);
}
\(BST\)的插入:
  • 为了方便,我们假设即将插入的点的权值在树中不存在。
  • 与检索过程类似,当发现要走向的子节点为空的时候,直接建立关键码为\(val\)的新节点为\(p\)的子节点。
void ins(int &p, int val)
{
if(p == 0)
{
p = New(val);
return;
}
if(val == a[p].val) return;
if(val < a[p].val) ins(a[p].l, val);
else ins(a[p].r, val);
}
\(BST\)求前驱/后继:
  • \(val\)的前驱指小于\(val\)且最大的数,\(val\)的后继指大于\(val\)且最小的数。

  • 求后继:

    • 初始化\(ans\)为具有正无穷关键码的那个节点的编号。然后在\(BST\)中检索\(val\)。在检索过程中,每经过一个节点,都检查节点的关键码,判断能否更新所求的后继。
    • 检索完成有三种可能:
    • \(1:\) 没有找到\(val\),此时\(val\)的后继就在已经经过的节点中。此时\(ans\)为所求。
    • \(2:\) 找到了关键码为\(val\)的节点\(p\),但\(p\)没有右子树。那么此时的\(ans\)为所求。
    • \(3:\) 找到了关键码为\(val\)的节点\(p\),且\(p\)有右子树,那么就从\(p\)的右孩子出发一直向左走,就找到了\(val\)的后继。
    int GetNext(int val)
    {
    int ans = 2; // a[2].val = INF
    int p = root;
    while(p)
    {
    if(val == a[p].val)
    {
    if(a[p].r > 0)
    {
    p = a[p].r;
    while(a[p].l > 0) p = a[p].l;
    ans = p;
    }
    break;
    }
    if(a[p].val >val && a[p].val < a[ans].val) ans = p;
    p = val < a[p].val ? a[p].l : a[p].r;
    }
    return a[ans].val;
    }
\(BST\)的节点删除:
  • 从\(BST\)中删除节点权值为\(val\)的数字。
  • 首先检索得到权值为\(val\)的节点\(p\)。
  • 节点\(p\)的子节点数目小于\(2\),则直接删除该节点,并让子节点代替\(p\)的位置。
  • 若\(p\)节点又有左子树又有右子树,则求出\(p\)的后继节点\(next\)。根据我们上面的分析,后继是进入右子树后一直往左走,那么这个后继节点\(next\)是不会有左子树的。这一点可以用反证法简单的证明一下。
    • 如果此时的\(next\)节点有左子树,此时再来看后继的定义:大于\(val\)的最小数,那么此时\(next\)再往左走能得到大于\(val\)的且比\(next\)节点权值更小的一个数,即\(next\)不为\(p\)的后继节点。矛盾。
  • 此时我们直接删除节点\(next\)(删除之前要记录一下),再用\(next\)的右子树代替节点\(next\)的位置,再删除\(p\),再用记录下来的\(next\)代替\(p\)的位置。
其他:
  • 来解决一下为什么插入两个特殊的节点能减少特判的原因。

  • 在主观上,我们去画图模拟这样一棵\(BST\),有没有这两个节点是无所谓的。但是到了代码实现里,插入这两个节点却极大的方便了我们代码的编写。

  • 手动模拟插入几个节点试试看会是什么样:

  • 这样的话十分清楚了,如果我们插入的时候还没有节点我们也可以直接插入,检索前驱后继也可以分别从一号或者二号节点开始,即使删除操作把点全部删光了依然会有两个节点在那。我们每次操作只需要关注我们想做的事,不需要关注什么乱七八糟的有没有节点啊,树是不是空的啊,查找前驱我应该从哪里开始啊等等等等的问题,就能完完全全根据伪代码写。(话糙理不糙)【划掉。

\(BST\)复杂度分析
\(BST\)时间复杂度分析:
  • 一棵\(N\)个节点的二叉树深度一般为\(logN\),所以他也能保证每一次操作的复杂度为\(O(logN)\)。
  • 但是当遇到插入一个有序序列的情况,那么\(BST\)将退化成一条链,操作的复杂度也将退化为\(O(n)\)。
  • 为了解决这个问题,保证\(BST\)的平衡,产生了各种平衡树如\(AVL\)树,红黑树,替罪羊树等。
    • (这三个除了红黑树以外我都没了解过\(QwQ\))
  • 其中入门级别的平衡树是\(Treap\),较为常用的是\(Splay\)。本着入门的精神,接下来我们来看看\(Treap\)。

\(Treap\)

旋转操作:
  • 对于一个序列\(1,2,3,4,5\),或者乱序成\(2,3,1,5,4\)。将它们插入\(BST\)中,得到的\(BST\)其实是等价的。但是第一种显然在复杂度上不占优。

  • 我们将通过变形来更改\(BST\)的形态但同时让他保持原来的信息这样的操作,叫做"旋转"。旋转又分为左旋和右旋。

  • 可以结合此图加深理解

  • 即我们可以通过旋转来将原本退化的\(BST\)通过等价变换成为一棵能保证复杂度的树。

什么样旋转是合理的?
  • 在随机数据下,一棵\(BST\)就是平衡的。所以\(treap\)的思想就是利用随机来创造平衡条件。
  • \(treap\)是\(tree\)和\(heap\)的结合。所以\(treap\)有两个关键字,一个是随机附上的数值,一个是原本的权值。
  • 当我们插入一个新结点的时候,我们对其附上一个随机值,然后像二叉堆那样进行左旋或者右旋来进行调整,这样既能保证平衡性又能保证\(BST\)性质。
    • 好像有点靠脸\(AC\)的意思?【划掉
    • 但只要没有被大佬\(\%\)而导致\(rp-=INF\)问题应该也不大。
    • \(Splay\)是一个不错的选择,更通用,也不会因为随机建值靠脸拿分。

模板题:

Acwing255: 普通平衡树

洛谷3369: 普通平衡树

  • 两个地址提供的是一道题,但建议两个地方都交一下。

  • 代码模板源于蓝书,详细注释

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 10;
const int INF = 0x3f3f3f3f; int tot, root, n;
struct Treap
{
int l, r; //左右子节点
int val, dat; //Bst的权值 堆的权值 满足大根堆性质
int cnt, sz; //重复的数量 子树(包括根节点)的大小
}a[maxn]; //新建一个节点
int New(int val)
{
a[++tot].val = val;
a[tot].dat = rand(); //给堆的那一部分附上随机值
a[tot].cnt = a[tot].sz = 1; //此时重复数和子树都为1
return tot;
} void update(int p){ //更新当前父节点的信息
a[p].sz = a[a[p].l].sz + a[a[p].r].sz + a[p].cnt;
} //初始建立treap 其中有两个特殊节点
void Build()
{
New(-INF), New(INF);
root = 1; a[1].r = 2;
update(root); //更新根节点
} int GetRankByVal(int p, int val)
{
if(p == 0) return 0; //如果没有这个节点
//如果此时找到了 那么排名就为小于他的数字+自身
if(val == a[p].val) return a[a[p].l].sz + 1;
//可以直接向左走
if(val < a[p].val) return GetRankByVal(a[p].l, val);
//向右走的话其实就相当于左半部分加上根全部小于他
return GetRankByVal(a[p].r, val) + a[a[p].l].sz + a[p].cnt;
} //查找排名为rk的数
int GetValByRank(int p, int rk)
{
if(p == 0) return INF; //空节点
//如果当前节点左子树的规模大于rk
//那么说明答案一定在左子树中
if(a[a[p].l].sz >= rk)
return GetValByRank(a[p].l, rk);
if(a[a[p].l].sz + a[p].cnt >= rk)
return a[p].val;
//向右走的时候rk要减去左子树和根节点的规模
return GetValByRank(a[p].r, rk - a[a[p].l].sz - a[p].cnt);
} void zig(int &p) //右旋操作
{
int q = a[p].l; //左子树
a[p].l = a[q].r; //左子树的右子树拼接到根节点的左子树上
a[q].r = p; //左子树的右子树变为根节点 左子树的左子树不变
p = q; //把原来左子树的信息换过去
update(a[p].r); update(p);//例行更新两个父节点的信息
} void zag(int &p) //左旋操作 同理与右旋
{
int q = a[p].r;
a[p].r = a[q].l; a[q].l = p; p = q;
update(a[p].l); update(p);
} //插入和删除操作 一般会涉及到旋转
//所以这时候采用递归写法 以便于更新节点信息
void ins(int &p, int val)
{
if(p == 0)
{
p = New(val);
return;
}
if(val == a[p].val)
{
a[p].cnt++; update(p);
return;
}
if(val < a[p].val)
{
ins(a[p].l, val); //因为需要满足大根堆性质
//当我的左子节点的堆权值大于根节点的时候
//右旋把左子节点换上来
if(a[p].dat < a[a[p].l].dat) zig(p);
}
else
{
ins(a[p].r, val);
if(a[p].dat < a[a[p].r].dat) zag(p);
}
update(p);
} //找到需要删除的节点并将其下旋至叶子节点后删除
//减少维护节点信息等复杂问题
void del(int &p, int val)
{
if(p == 0) return;
if(val == a[p].val)//检测到了val
{
if(a[p].cnt > 1)
{
a[p].cnt--, update(p); //直接减掉一个副本即可
return; //退出
}
if(a[p].l || a[p].r)//不是叶子节点 向下旋转
{
if(a[p].r == 0 || a[a[p].l].dat > a[a[p].r].dat)
{zig(p); del(a[p].r, val);} //右旋 后进入右子树
else {zag(p); del(a[p].l, val);}
update(p);//例行更新父节点信息
}
else p = 0; //是叶子节点 直接删除
return;
}
val < a[p].val ? del(a[p].l, val) : del(a[p].r, val);
update(p);
} int GetPre(int val)
{ //前驱:小于x且最大的数
int ans = 1; //a[1].val = -INF
int p = root;
while(p)
{
if(val == a[p].val) //找到了节点值相同的
{
if(a[p].l > 0) //如果有左子树
{
p = a[p].l; //不停向右走
while(a[p].r > 0) p = a[p].r;
ans = p;
}
break; //此时的ans为答案
}
//就算找不到val 前驱也被ans经过了
if(a[p].val < val && a[p].val > a[ans].val) ans = p;
p = val < a[p].val ? a[p].l : a[p].r;
}
return a[ans].val;
} int GetNext(int val)
{ //后继:大于x且最小的数
int ans = 2; // a[2].val = INF
int p = root;
while(p)
{
if(val == a[p].val)
{
if(a[p].r > 0)
{
p = a[p].r;
while(a[p].l > 0) p = a[p].l;
ans = p;
}
break;
}
if(a[p].val >val && a[p].val < a[ans].val) ans = p;
p = val < a[p].val ? a[p].l : a[p].r;
}
return a[ans].val;
} int main()
{
Build();
scanf("%d", &n);
int op, x;
while(n--)
{
scanf("%d%d", &op, &x);
if(op == 1)
{//插入x数
ins(root, x);
}
if(op == 2)
{//删除x(若有多个x,只删除一个)
del(root, x);
}
if(op == 3)
{//查询x的排名(排名定义为比当前数小的个数+1)
//若有多个相同的数 输出最小的排名
printf("%d\n", GetRankByVal(root, x) - 1);
}
if(op == 4)
{//查询排名为x的数
printf("%d\n", GetValByRank(root, x + 1));
}
if(op == 5)
{//求x的前驱(小于x且最大的数)
printf("%d\n", GetPre(x));
}
if(op == 6)
{//求x的后继(大于x且最小的数)
printf("%d\n", GetNext(x));
}
}
return 0;
}

入门平衡树: Treap的更多相关文章

  1. hiho #1325 : 平衡树·Treap

    #1325 : 平衡树·Treap 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似. 小Hi:你说的是哪两个啊? ...

  2. hiho一下103周 平衡树·Treap

    平衡树·Treap 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似. 小Hi:你说的是哪两个啊? 小Ho:就是二 ...

  3. 算法模板——平衡树Treap 2

    实现功能:同平衡树Treap 1(BZOJ3224 / tyvj1728) 这次的模板有了不少的改进,显然更加美观了,几乎每个部分都有了不少简化,尤其是删除部分,这个参照了hzwer神犇的写法,在此鸣 ...

  4. 【山东省选2008】郁闷的小J 平衡树Treap

    小J是国家图书馆的一位图书管理员,他的工作是管理一个巨大的书架.虽然他很能吃苦耐劳,但是由于这个书架十分巨大,所以他的工作效率总是很低,以致他面临着被解雇的危险,这也正是他所郁闷的.具体说来,书架由N ...

  5. Hihocoder 1325 平衡树·Treap(平衡树,Treap)

    Hihocoder 1325 平衡树·Treap(平衡树,Treap) Description 小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似. 小Hi:你说的是哪两个啊? 小Ho:就是二叉 ...

  6. HihoCoder 1325 平衡树·Treap

    HihoCoder 1325 平衡树·Treap 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似. 小Hi:你说 ...

  7. 普通平衡树Treap(含旋转)学习笔记

    浅谈普通平衡树Treap 平衡树,Treap=Tree+heap这是一个很形象的东西 我们要维护一棵树,它满足堆的性质和二叉查找树的性质(BST),这样的二叉树我们叫做平衡树 并且平衡树它的结构是接近 ...

  8. HihoCoder1325 : 平衡树·Treap(附STL版本)

    平衡树·Treap 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Ho:小Hi,我发现我们以前讲过的两个数据结构特别相似. 小Hi:你说的是哪两个啊? 小Ho:就是二 ...

  9. luoguP3369[模板]普通平衡树(Treap/SBT) 题解

    链接一下题目:luoguP3369[模板]普通平衡树(Treap/SBT) 平衡树解析 #include<iostream> #include<cstdlib> #includ ...

随机推荐

  1. 【题解】Luogu P5291 [十二省联考2019]希望

    ytq鸽鸽出的题真是毒瘤 原题传送门 题目大意: 有一棵有\(n\)个点的树,求有多少方案选\(k\)个联通块使得存在一个中心点\(p\),所有\(k\)个联通块中所有点到\(p\)的距离都\(\le ...

  2. 【mysql】mysql5.7支持的json字段查询【mybatis】

    mysql5.7支持的json字段查询 参考:https://www.cnblogs.com/ooo0/p/9309277.html 参考:https://www.cnblogs.com/pfdltu ...

  3. c#创建windows服务(代码方式安装、启动、停止、卸载服务)

    转载于:https://www.cnblogs.com/mq0036/p/7875864.html 一.开发环境 操作系统:Windows 10 X64 开发环境:VS2015 编程语言:C# .NE ...

  4. BUAA-OO-2019 第一单元总结

    第一次作业 第一次作业需要完成的任务为简单多项式导函数的求解. 思路 因为仅仅是简单多项式的求导,所以求导本身没有什么可说的,直接套用幂函数的求导公式就行了,主要的精力是花在了正则表达式上.这里推荐两 ...

  5. 【转载】Asp.Net生成图片验证码工具类

    在Asp.Net应用程序中,很多时候登陆页面以及其他安全重要操作的页面需要输入验证码,本文提供一个生成验证码图片的工具类,该工具类通过随机数生成验证码文本后,再通过C#中的图片处理类位图类,字体类,一 ...

  6. Qt--多线程间的互斥

    一.多线程间的互斥 临界资源--每次只允许一个线程进行访问的资源 线程间的互斥--多个线程在同一个时刻需要访问临界资源 QMute类是一把线程锁,保证线程间的互斥--利用线程锁能够保证临界资源的安全性 ...

  7. shell:echo -e "\033字体颜色"

    格式: echo -e "\033[字背景颜色;字体颜色m字符串\033[0m" 例如: echo -e "\033[41;36m 你好 \033[0m" 其中 ...

  8. Cheat Engine 人造指针

    打开游戏 查看内存区域 查看游戏当前使用的内存区域 下面这一段是游戏当前使用的内存区域,选择一片可以读写的内存区域 跳转到这片内存 查看是否有空余内存可以使用 使用空闲内存 我们选择0075DFD0开 ...

  9. Cheat Engine 修改汇编指令

    打开游戏 扫描阳光 扫描过程就不讲了 找到阳光的地址 显示反汇编 找到使阳光减少的反汇编代码 空指令替换 将阳光减少汇编指令,用空指令替换.这样阳光就不再减少了 指令替换 也可以将汇编指令修改,减少变 ...

  10. FreeRTOS 任务通知模拟二值信号量

    FreeRTOS官方统计,使用任务通知替代二值信号量的时候,任务解除阻塞的时间要快45%,并且需要的RAM也更少 举例 void DataProcess_task(void *pvParameters ...