Treap=Tree+Heap。Treap是一棵二叉排序树,它的左子树和右子树分别是一个Treap,和一般的二叉排序树不同的是, Treap记录一个额外的数据, 就是优先级。Treap在以关键码构成二叉排序树的同时,还满足堆的性质(在这里我们假设节点的优先级大于该节点的孩子的优先级)。但是这里要注意的是Treap和二叉堆有一点不同,就是 二叉堆必须是完全二叉树,而Treap不一定是完全二叉树。 该博文主要讲解了Treap树相关的知识点及其实现。

Treap

  我们都知道,在二叉查找树中,当插入的数据为随机的时候,其有较好的性能使得孩子节点大体上左右均匀分布。但是,当插入的数据为有序的时候,其会退化为一个链表的状态,从而达不到效果。一般,我们有AVL或者红黑树以及Splay等平衡二叉树结构。但是,其在实现上难度较高。为此,引入了Treap,treap采用了二叉堆的性质来保持二叉树的平衡性。因为对于一个堆而言,其需要满足如下的性质:一个节点的两个孩子节点的值都小于节点的本身的值。但是,对于一棵二叉查找树而言,其需要满足一棵树的左孩子节点的值小于根节点的值并且右孩子节点的值大于根节点的值。这显然出现了冲突。为此,我们需要增加一个变量,用于满足这样的性质。我们将用于满足二叉搜索树的性质的值称之为key,用于满足堆的性质的值称之为priority(优先级)。

  每个节点的key我们是无法改变的,必须要按照要求来,但是为了保持treap的平衡性,我们只能从priority上做文章。其实也并没有复杂之处,就是让每个节点的priority都取一个随机值,这样我们就可以保证这棵树“基本平衡”。

treap的基本操作

  为了要满足二叉堆和平衡树的性质,treap中有两种操作用于将不满足二叉堆的性质的二叉查找树进行相应的调整,使其满足相应的性质。这两个操作分别为左旋和右旋:

旋转操作的目的是使得不满足堆序的两个节点通过调整位置,使其重新满足堆序。且不改变二叉查找树的性质。

ps:我们知道,堆有两种形式,一种是大顶堆,一种是小顶堆,以下讨论的,我们采用小顶堆的方式对其进行操作

treap的相关操作

  在对treap树进行相关的操作之前,我们先定义treap的节点类:

class Node{
/**
* 元素的关键字值
*/
T key;
/**
* 节点的优先级,用于满足堆的性质
*/
int priority;
/**
* 该节点的左右孩子节点
*/
Node left;
Node right;
/**
* 一个随机数生成器,用于随机生成节点的元素的优先级
*/
Random random=new Random();
public Node(T key){
this(key,null,null);
} public Node(T key,Node left,Node right){
this.key=key;
this.left=left;
this.right=right;
this.priority=random();
}

  treap相关的操作有插入、删除、查找。

查找:

  treap的查找操作并不影响treap树相关的性质,其只需要按照普通的二叉查找树的检索方式进行查找即可。

具体代码如下:

/**
* 用于treap树的查找操作
* @param key 需要进行查找的关键字
* @return 查找的相应的节点,当没有找到对应的节点时,其返回null
*/
public Node search(T key){
Node temp=this.root;
while(temp!=null){
int cmp=key.compareTo(temp.key);
if(cmp<0){
temp=temp.left;
}
else if(cmp>0){
temp=temp.right;
}
else{
break;
}
}
return temp;
}

插入:

  在Treap中插入元素,与在BST中插入方法相似。首先找到合适的插入位置,然后建立新的节点,存储元素。但是要注意建立新的节点的过程中,会随机地生成一个修正值,这个值可能会破坏堆序,因此我们要根据需要进行恰当的旋转。具体方法如下:

  1. 从根节点开始插入;

  2. 如果要插入的值小于等于当前节点的值,在当前节点的左子树中插入,插入后如果左子节点的修正值小于当前节点的修正值,对当前节点进行右旋;

  3. 如果要插入的值大于当前节点的值,在当前节点的右子树中插入,插入后如果右子节点的修正值小于当前节点的修正值,对当前节点进行左旋;

  4. 如果当前节点为空节点,在此建立新的节点,该节点的值为要插入的值,左右子树为空,插入成功。

其过程如下例:

具体代码如下:

/**
* 用于往treap树中插入相应的节点
* @param key 节点元素的值
*/
public void insert(T key){
//当节点元素存在的时候
if(key==null||search(key)!=null){
return;
}
this.root=insert(this.root,key);
} /**
* 用于插入元素相应的节点值
* @param node 需要进行比较的节点
* @param key 需要进行插入的键值
* @return 树对应的根节点
*/
private Node insert(Node node,T key){
if(node==null){
node=new Node(key);
}
else if(key.compareTo(node.key)<0){
node.left=insert(node.left,key);
//当满足情况的时候,对其进行旋转操作
if(node.left.priority<node.priority){
node=rotateRight(node);
}
}
else{
node.right=insert(node.right,key);
//当满足情况的时候,对其进行旋转操作
if(node.right.priority<node.priority){
node=rotateLeft(node);
}
}
return node;
}

删除:

  与BST一样,在Treap中删除元素要考虑多种情况。我们可以按照在BST中删除元素同样的方法来删除Treap中的元素,即用它的后继(或前驱)节点的值代替它,然后删除它的后继(或前驱)节点并对其进行相应的旋转调整操作使其符合Treap中堆性质的要求。为了不使Treap向一边偏沉,我们需要随机地选取是用后继还是前驱代替它,并保证两种选择的概率均等。上述方法期望时间复杂度为O(logN),但是这种方法并没有充分利用Treap已有的随机性质,而是重新得随机选取代替节点。我们给出一种更为通用的删除方法,这种方法是基于旋转调整的。首先要在Treap树中找到待删除节点的位置,然后分情况讨论:

情况一, 该节点为叶节点或链节点(即只有一个孩子节点的节点),则该节点是可以直接删除的节点。若该节点有非空子节点,用非空子节点代替该节点的,否则用空节点代替该节点,然后删除该节点。

情况二, 该节点有两个非空子节点。我们的策略是通过旋转,使该节点变为可以直接删除的节点。如果该节点的左子节点的修正值小于右子节点的修正值,右旋该节点,使该节点降为右子树的根节点,然后访问右子树的根节点,继续讨论;反之,左旋该节点,使该节点降为左子树的根节点,然后访问左子树的根节点,继续讨论,直到变成可以直接删除的节点。(也就是让该节点的左右孩子节点中的最小优先级的节点称为该节点)

其过程如下例:

具体代码如下:

/**
* 节点的删除操作
* @param key 需要进行删除的节点的键值
*/
public void remove(T key){
//用于判空操作
if(key==null||search(key)==null){
return;
}
this.root=remove(this.root,key);
} /**
* 删除对应的节点
* @param node 开始进行比较的节点
* @param key 进行删除的节点的关键字
* @return 其子树的根节点
*/
private Node remove(Node node,T key){
if(node==null){
return null;
}
//左子树
if (key.compareTo(node.key)<0){
node.left=remove(node.left,key);
}
//右子树
if(key.compareTo(node.key)>0){
node.right=remove(node.right,key);
}
//相等的情况,即删除的节点为该节点时
if(key.compareTo(node.key)==0){
//当存在左右孩子节点的时候
if(node.left!=null&&node.right!=null){
//如果左孩子优先级低就右旋
if(node.left.priority<node.right.priority){
node=rotateRight(node);
}
else{
node=rotateLeft(node);
}
//旋转后继续进行删除操作
node=remove(node,key);
}
else{
//当其为根节点的时候
if(node.left==null&&node.right==null){
return null;
}
//当其为单分支树的时候
node=node.left==null?node.right:node.left;
}
}
return node;
}

完整代码如下:

import java.util.Random;

/**
* @author 学徒
*用于实现Treap
*/
public class Treap<T extends Comparable<T>>{
/**
* 树的根节点
*/
private Node root;
/**
* 树的节点类
*/
private class Node{
/**
* 元素的关键字值
*/
T key;
/**
* 节点的优先级,用于满足堆的性质
*/
int priority;
/**
* 该节点的左右孩子节点
*/
Node left;
Node right;
/**
* 一个随机数生成器,用于随机生成节点的元素的优先级
*/
Random random=new Random();
public Node(T key){
this(key,null,null);
} public Node(T key,Node left,Node right){
this.key=key;
this.left=left;
this.right=right;
this.priority=random();
} /**
* 用于随机获取节点的优先级的随机数生成函数
* @return 随机数值
* 参考自:https://blog.csdn.net/chen_tr/article/details/50924073 随机数的生成。这样做,据说可以使得随机数的值
* 可以不发生重复
*/
private int random(){
int seed=random.nextInt();
return (int)(seed*48271L%Integer.MAX_VALUE);
}
} /**
* 用于其相应的左旋操作
* @param node 进行旋转的节点
* @return 旋转后的根节点
*/
private Node rotateLeft(Node node){
Node temp=node.right;
node.right=temp.left;
temp.left=node;
return temp;
}
/**
* 用于其相应的右旋操作
* @param node 进行旋转的节点
* @return 旋转后的根节点
*/
private Node rotateRight(Node node){
Node temp=node.left;
node.left=temp.right;
temp.right=node;
return temp;
} /**
* 用于treap树的查找操作
* @param key 需要进行查找的关键字
* @return 查找的相应的节点,当没有找到对应的节点时,其返回null
*/
public Node search(T key){
Node temp=this.root;
while(temp!=null){
int cmp=key.compareTo(temp.key);
if(cmp<0){
temp=temp.left;
}
else if(cmp>0){
temp=temp.right;
}
else{
break;
}
}
return temp;
} /**
* 用于往treap树中插入相应的节点
* @param key 节点元素的值
*/
public void insert(T key){
//当节点元素存在的时候
if(key==null||search(key)!=null){
return;
}
this.root=insert(this.root,key);
} /**
* 用于插入元素相应的节点值
* @param node 需要进行比较的节点
* @param key 需要进行插入的键值
* @return 树对应的根节点
*/
private Node insert(Node node,T key){
if(node==null){
node=new Node(key);
}
else if(key.compareTo(node.key)<0){
node.left=insert(node.left,key);
//当满足情况的时候,对其进行旋转操作
if(node.left.priority<node.priority){
node=rotateRight(node);
}
}
else{
node.right=insert(node.right,key);
//当满足情况的时候,对其进行旋转操作
if(node.right.priority<node.priority){
node=rotateLeft(node);
}
}
return node;
} /**
* 节点的删除操作
* @param key 需要进行删除的节点的键值
*/
public void remove(T key){
//用于判空操作
if(key==null||search(key)==null){
return;
}
this.root=remove(this.root,key);
} /**
* 删除对应的节点
* @param node 开始进行比较的节点
* @param key 进行删除的节点的关键字
* @return 其子树的根节点
*/
private Node remove(Node node,T key){
if(node==null){
return null;
}
//左子树
if (key.compareTo(node.key)<0){
node.left=remove(node.left,key);
}
//右子树
if(key.compareTo(node.key)>0){
node.right=remove(node.right,key);
}
//相等的情况,即删除的节点为该节点时
if(key.compareTo(node.key)==0){
//当存在左右孩子节点的时候
if(node.left!=null&&node.right!=null){
//如果左孩子优先级低就右旋
if(node.left.priority<node.right.priority){
node=rotateRight(node);
}
else{
node=rotateLeft(node);
}
//旋转后继续进行删除操作
node=remove(node,key);
}
else{
//当其为根节点的时候
if(node.left==null&&node.right==null){
return null;
}
//当其为单分支树的时候
node=node.left==null?node.right:node.left;
}
}
return node;
}
}

参考自: treap的分析与应用以及6天通吃树结构—— 第三天 Treap树

K:Treap(堆树)的更多相关文章

  1. Treap(树堆)入门

    作者:zifeiy 标签:Treap 首先,我么要知道:Treap=Tree+Heap. 这里: Tree指的是二叉排序树: Heap指的是堆. 所以在阅读这篇文章之前需要大家对 二叉查找树 和 堆( ...

  2. treap(堆树)

    # 2018-09-27 17:35:58 我实现的这个treap不能算是堆.有问题 最近对堆这种结构有点感兴趣,然后想用指针的方式实现一个堆而不是利用数组这种结构,于是自己想到了一个用二叉树结构实现 ...

  3. [模板] 平衡树: Splay, 非旋Treap, 替罪羊树

    简介 二叉搜索树, 可以维护一个集合/序列, 同时维护节点的 \(size\), 因此可以支持 insert(v), delete(v), kth(p,k), rank(v)等操作. 另外, prev ...

  4. NOI 2015 荷马史诗【BZOJ 4198】k叉Huffman树

    抱歉因为NOIP集训,好长时间没再写题解了. NOI 2015也就只有这道题一看就能懂了-- 4198: [Noi2015]荷马史诗 Time Limit: 10 Sec  Memory Limit: ...

  5. 「模板」「讲解」Treap名次树

    Treap实现名次树 前言 学平衡树的过程可以说是相当艰难.浏览Blog的过程中看到大量指针版平衡树,不擅长指针操作的我已经接近崩溃.于是,我想着一定要写一篇非指针实现的Treap的Blog. 具体如 ...

  6. 计蒜客 38229.Distance on the tree-1.树链剖分(边权)+可持久化线段树(区间小于等于k的数的个数)+离散化+离线处理 or 2.树上第k大(主席树)+二分+离散化+在线查询 (The Preliminary Contest for ICPC China Nanchang National Invitational 南昌邀请赛网络赛)

    Distance on the tree DSM(Data Structure Master) once learned about tree when he was preparing for NO ...

  7. Cogs 1345. [ZJOI2013] K大数查询(树套树)

    [ZJOI2013] K大数查询 /* 树套树写法. bzoj过不了. 可能有负数要离散吧. 线段树套线段树. 外层权值线段树,内层区间线段树维护标记. 对权值建一棵权值线段树. 某个点表示权值在某个 ...

  8. 查找——图文翔解Treap(树堆)

    之前我们讲到二叉搜索树,从二叉搜索树到2-3树到红黑树到B-树. 二叉搜索树的主要问题就是其结构与数据相关,树的深度可能会非常大,Treap树就是一种解决二叉搜索树可能深度过大的还有一种数据结构. T ...

  9. zoj 2112 Dynamic Rankings 动态第k大 线段树套Treap

    Dynamic Rankings Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://acm.zju.edu.cn/onlinejudge/show ...

随机推荐

  1. BZOJ 1002--[FJOI2007]轮状病毒(高精度)

    1002: [FJOI2007]轮状病毒 Time Limit: 1 Sec  Memory Limit: 162 MBSubmit: 6858  Solved: 3745[Submit][Statu ...

  2. 使用Navicat 创建mysql存储过程,实现日期加流水号序列

    目的:使用Navicat 创建mysql存储过程,实现格式为8位日期(年月日)+5位流水号序列. 步骤: 1.打开Navicat 登录数据库,点击导航栏上的函数,如下图: 2.点击新建函数,选择“过程 ...

  3. Elasticsearch系列(五)----JAVA客户端之TransportClient操作详解

    Elasticsearch JAVA操作有三种客户端: 1.TransportClient 2.JestClient 3.RestClient 还有种是2.3中有的NodeClient,在5.5.1中 ...

  4. 【wireshark】总体结构

    1. 总体结构 wireshark的总体结构如下图所示. 2. 功能模块 模块名 功能 源码子目录 GTK/Qt 处理所有的用户输入/输出(所有的窗口,对话框等等) /ui GTK: /ui/gtk ...

  5. 解决 ArchLinux 下中文 Chinese 不能输入 couldnt input 的问题

    解决 ArchLinux 下中文 Chinese 不能输入 couldnt input 的问题 一.Question 一年多的 ArchLinux 用户再次回归.然鹅,见面礼就是终端不能输入中文. 在 ...

  6. 如何在for循环中使用多线程

    import java.util.concurrent.Executor;import java.util.concurrent.Executors; public class Test {priva ...

  7. 解决ajax跨域问题的一种方法

    解决ajax跨域问题的一种方法 前后端分离经常用json来传输数据,比较常见的问题就有ajax跨域请求的错误问题,这里是我的一种解决方法: 在java中加入如下的注解类: import org.spr ...

  8. Linux vim 编辑命令

    vi命令命令模式:yy:复制 光标所在的这一行 4yy:复制 光标所在行开始向下的4行p: 粘贴dd:剪切 光标所在的这一行2dd:剪切 光标所在行 向下 2行D:从当前的光标开始剪切,一直到行末d0 ...

  9. Android 开发工具类 02_DensityUtils

    常用单位转换的辅助类: 1.dp 转 px: 2.sp 转 px: 3.px 转 dp: 4.px 转 sp. import android.content.Context; import andro ...

  10. 快速搭建gulp项目实战

    gulp是前端开发过程中对代码进行构建的工具,是自动化项目的构建利器:她不仅能对网站资源进行优化,而且在开发过程中很多重复的任务能够使用正确的工具自动完成:使用她,我们不仅可以很愉快的编写代码,而且大 ...