一.Treap的性质
Treap就是集BST、Heap二者的性质于一身,即能够支持BST的操作,有能够保证Heap的深度。但BST和Heap的性质似乎有些矛盾,前者是左子树<根<右子树,后者是根<左儿子<右儿子(小顶堆)。
其实Treap的本质还是BST,对于任意节点,保证根左侧子树的所有节点比根小,右侧的所有节点比根大的树(没有相同节点),Treap只是利用堆的性质,赋予每一个节点一个随机值,按照随机值维护堆的形状,即在插入的情况中,如果树A的根节点的权重比树B的大,那树B就一定是树A的子树,再根据树B的根节点的值的大小确定是在树A的左子树还是右子树。
二.节点的结构
左右子树、值、权重、树的大小
三.核心操作一:分裂split
分为两种形式:按值分裂和按大小分裂
按值分裂:将树拆成两棵树,拆出来的一棵树的值全部小于等于给定的值,另一棵树的值全部大于给定的值;
按大小分裂:将树拆成两棵树,拆出来的一棵树的节点数量等于给定的大小,另一棵树就包括剩余部分;
对应到具体实现上就是用两个节点变量接收分裂后的两个根节点就行
一般来说,在使用fhq Treap当平衡树用的时候就是按值分裂,而在维护区间信息的时候就是按大小分裂(例如文艺平衡树)
四.核心操作二:合并merge
合并就是将两个树合并成一棵树,其中x树上的全部值小于等于y树上的所有值,并且合并出来的树依然满足Treap的性质,具体逻辑可见下面的代码,文字不想写了
基于这两种操作就可以有插入、删除、查询一个数在树对应的有升序序列中的排名、依据排名获取相应的节点以及获取前驱后继的函数
五.一般Treap(按值分裂)
import java.security.SecureRandom;
import java.util.ArrayList;
public class FHQTreap<T extends Comparable<T>> {
private TreapNode<T> root;//当前的根节点
private SecureRandom random = new SecureRandom();//用法:random.nextInt(range+1)——生成处以0-range的随机数
private static final int range = 100;//后续的权值就设置为0-100内的随机数吧
public FHQTreap() {
root = null;
}
public void insert(T val) {
//1.从根节点开始,按照val进行值分裂,得到part1是值小于等于val的全部节点组成的树,part2是值大于val的
ArrayList<TreapNode<T>> res = splitByVal(root, val);
//2.合并
root = merge(merge(res.get(0), new TreapNode<>(val)), res.get(1));
}
public void remove(T val) {
//1.从根节点开,按照val值进行值分裂,得到res[0]是值小于等于val的全部节点组成的树,res[1]是值大于val的
ArrayList<TreapNode<T>> res = splitByVal(root, val);
//2.再对res[0]按照val-1进行值分裂,得到res2[0]是值小于val的全部节点组成的树,res2[1]是值等于val的
ArrayList<TreapNode<T>> res2 = splitByVal2(res.get(0), val);
//3.删除res2[1]的根节点,即将res2[1]的左右子树合并得到新节点给到part3
TreapNode<T> res21 = res2.get(1);
TreapNode<T> tmpNode = merge(res21.left, res21.right);
//4.合并,更新root
root = merge(merge(res2.get(0), tmpNode), res.get(1));
}
private ArrayList<TreapNode<T>> splitByVal(TreapNode<T> node, T val) {
//返回的数组中,第一个表示小于等于val的节点,第二个是大于val的
ArrayList<TreapNode<T>> res = new ArrayList<>();
if(node == null) {
res.add(null);
res.add(null);
} else {
int cmp = node.value.compareTo(val);
if(cmp <= 0) {
//提醒:本质上还是BST,即当前节点的左子树的值小于当前节点的值,右子树的值大于等于当前节点的值
//当前节点的值小于传入的值,而res[0]是小于等于当前节点的全部节点的树,则当前节点以及其左子树都是属于res[0]的
//那此时就是要向右子节点走下去了,res[0]就可以先确定为node了,然后往右子节点递归判断得到的属于res[0]的节点就
//放到res[0]的右子树上
ArrayList<TreapNode<T>> tmp1 = splitByVal(node.right, val);
node.right = tmp1.get(0);
res.add(node);
res.add(tmp1.get(1));
} else {
ArrayList<TreapNode<T>> tmp2 = splitByVal(node.left, val);
node.left = tmp2.get(1);
res.add(tmp2.get(0));
res.add(node);
}
int leftSize = node.left != null ? node.left.size : 0;
int rightSize = node.right != null ? node.right.size : 0;
node.size = leftSize + rightSize + 1;
}
return res;
}
private ArrayList<TreapNode<T>> splitByVal2(TreapNode<T> node, T val) {
//返回的数组中,第一个表示小于val的节点,第二个是大于等于val的
//这个函数的主要是为了解决泛型的val不能减一的问题,就是后面那个删除那里的,本来是从小于等于val的
// 树分裂出小于等于val-1和等于val的,但是泛型减不了,只能从这里的比较入手了,原理和上面那个几乎同名的函数是一样的
ArrayList<TreapNode<T>> res = new ArrayList<>();
if(node == null) {
res.add(null);
res.add(null);
} else {
int cmp = node.value.compareTo(val);
if(cmp < 0) {
ArrayList<TreapNode<T>> tmp1 = splitByVal2(node.right, val);
node.right = tmp1.get(0);
res.add(node);
res.add(tmp1.get(1));
} else {
ArrayList<TreapNode<T>> tmp2 = splitByVal2(node.left, val);
node.left = tmp2.get(1);
res.add(tmp2.get(0));
res.add(node);
}
int leftSize = node.left != null ? node.left.size : 0;
int rightSize = node.right != null ? node.right.size : 0;
node.size = leftSize + rightSize + 1;
}
return res;
}
private TreapNode<T> merge(TreapNode<T> node1, TreapNode<T> node2) {
//权值按照大顶堆
if(node1 == null) return node2;
else if (node2 == null) return node1;
else {
//这里要确保node1的值小于node2
if (node1.value.compareTo(node2.value) >= 0) {
TreapNode<T> tmp = node1;
node1 = node2;
node2 = tmp;
}
if (node1.weight > node2.weight) {
//node1的权重大于node2,那node2会成为node1的子节点,又node1的值小于node2的值,那node2就在node1的右子
node1.right = merge(node1.right, node2);
int leftSize = node1.left != null ? node1.left.size : 0;
int rightSize = node1.right != null ? node1.right.size : 0;
node1.size = leftSize + rightSize + 1;
return node1;
} else {
//node1的权重小于等于node2,那node1会成为node2的子节点,又node1的值小于node2的值,那node1就在node2的左子
node2.left = merge(node2.left, node1);
int leftSize = node2.left != null ? node2.left.size : 0;
int rightSize = node2.right != null ? node2.right.size : 0;
node2.size = leftSize + rightSize + 1;
return node2;
}
}
}
public int getRank(T val) {
ArrayList<TreapNode<T>> tmp = splitByVal(root, val);
TreapNode<T> tmpL = tmp.get(0);
while (tmpL.right != null) tmpL = tmpL.right;//一直找到最右
if(tmpL.value.equals(val)) return tmp.get(0).size;
else return tmp.get(0).size + 1;
}
public String getNodeByRank(int rank) {
//根据排名获取节点,只在删除标记为true的数找,不考虑那些打上删除标记的数
TreapNode<T> tmp = root;
while (tmp != null) {
//如果当前节点左子的存在节点和当前节点(如果存在)的存在节点刚好等于排名,那当前节点就是待查询的节点
int leftRealSize = tmp.left == null ? 0 : tmp.left.size;
if(leftRealSize + 1 == rank) break;
else if(leftRealSize >= rank) {
//如果当前节点的左子节点的存在节点大于等于排名,那待查询节点一定在左子树上
tmp = tmp.left;
} else {
//如果当前节点+左子树的存在节点的全部存在节点都比排名小,那就要到右子树去找
//这个时候就要查找以右子节点为根的树,排名要减去tmp节点和tmp左子树的存活节点
rank -= leftRealSize + 1;
tmp = tmp.right;
}
}
if(tmp == null) return "NoExist!";
return tmp.toString();
}
private void get_Pre(TreapNode<T> node) {
if(node == null) return;;
System.out.print("v:" + node.value+ " size:" + node.size + " w:" + node.weight);
System.out.println(" left:" + (node.left == null ? "无" : node.left.value)
+ " right:" + (node.right == null ? "无" : node.right.value));
get_Pre(node.left);
get_Pre(node.right);
}
public void getPre() {
get_Pre(root);
}
public class TreapNode<T extends Comparable<T>> {
T value;//节点所存储的值
int weight;//节点的权值,到插入的时候再分配
TreapNode<T> left;//左子节点
TreapNode<T> right;//右子节点
int size;//树的大小
public TreapNode(T value) {
this.value = value;
this.size = 1;
this.weight = random.nextInt(range + 1);//给新节点生成一个随机权值
}
@Override
public String toString() {
return "TreapNode{" +
"value=" + value +
", weight=" + weight +
", left=" + left +
", right=" + right +
", size=" + size +
'}';
}
}
}
public class test {
public static void main(String[] args) {
FHQTreap<Integer> fhq = new FHQTreap<>();
//1.insert test
// fhq.insert(1);
// fhq.insert(3);
// fhq.insert(5);
// fhq.insert(2);
// fhq.insert(4);
// fhq.getPre();
//2.remove test
// fhq.remove(3);
// System.out.println("删除3:");
// fhq.getPre();
// fhq.remove(2);
// System.out.println("删除2:");
// fhq.getPre();
//3.getRank test
// int SearchNum = 5;
// int rank = fhq.getRank(SearchNum);
// System.out.println(SearchNum + "的排名:" + rank);
//4.getNodeByRank test
// System.out.println(fhq.getNodeByRank(1));
// System.out.println(fhq.getNodeByRank(2));
// System.out.println(fhq.getNodeByRank(3));
// System.out.println(fhq.getNodeByRank(4));
}
}
六.文艺平衡树(按大小分裂)
维护区间[l,r]的具体步骤:
1.把Treap 按大小 l-1 拆成x和y两棵树;
2.再将y按 r-l+1 拆成y和z两棵树;
3.此时y树就是需要操作的区间,处理完后将上面三者合并回去就行
怎么解决文艺平衡树的问题呢?
需要用到线段树常用的应该概念:懒标记,每当一个区间需要翻转时,就对这个区间打上懒标记(如果已有懒标记就去掉懒标记,因为翻转两次相当于没翻转),在具体实现时,因为一个节点存储的是一个值,那个节点被打上懒标记的话,就意味着以这个节点为根的树的中序遍历的序列是需要翻转的,但是真正的翻转操作是要等到这个节点的子节点有可能被修改,或者需要输出整棵树对应的中序遍历时,再真正进行翻转(真正翻转的函数就是下传函数);
懒标记的下传操作——如果当前节点的左右子树在后续代码中有被更改风险时就要下传,就是对当前节点的左右子树进行交换,然后将翻转标记传给子树,当前节点的翻转标记就可以去除.
下传的意思应该是除了叶子节点不需要下传翻转标记的情况外,其他的节点都是只在当前节点完成左右子树交换,然后子树修改翻转标记的状态(修改的方式就是待翻转变不用翻转,不翻转变待翻转,即如果子树的翻转标记置为true,子树也不需要现在就执行真正的翻转,它要想真正翻转也是和上面的逻辑一样),并没有每一次都将树转换到理论上翻转后序列所对应的树。
import java.security.SecureRandom;
import java.util.ArrayList;
public class WenYiTree<T extends Comparable<T>> {
private TreapNode<T> root;//当前的根节点
private SecureRandom random = new SecureRandom();//用法:random.nextInt(range+1)——生成处以0-range的随机数
private static final int range = 100;//后续的权值就设置为0-100内的随机数吧
public WenYiTree() {
root = null;
}
public void reverse(int L, int R) {
if (R == L) {
return;
}
L = Math.max(Math.min(L, R), 1);
R = Math.min(Math.max(L, R), root.size);
System.out.println("翻转的区间为[" + L + "," + R + "]");
//翻转函数
ArrayList<TreapNode<T>> res1 = splitBySize(root, L - 1);
ArrayList<TreapNode<T>> res2 = splitBySize(res1.get(1), R - L + 1);
if (res2.get(0) != null) res2.get(0).lazyReverse = !res2.get(0).lazyReverse;
root = merge(merge(res1.get(0), res2.get(0)), res2.get(1));
}
private TreapNode<T> pushDown(TreapNode<T> node) {
if (node == null) return null;
else if (node.left == null && node.right == null) {
node.lazyReverse = false;
return node;
}
//下传操作,我的理解是就是将当前节点所对应的区间真正进行翻转操作,然后将需要翻转一次的标记传给子节点的翻转标记
//我觉得就是当要求在某一个区间进行翻转操作时,就是在那个区间对应的节点打上懒标记,而其子节点是没有懒标记的,除非等到
//父节点有懒标记然后执行真正的翻转操作后,其再将懒标记传给子节点
//这里就是执行真正的翻转操作,首先交换两个子树
TreapNode<T> tmp = node.left;
node.left = node.right;
node.right = tmp;
//将当前节点的懒标记往下传,即相当于对两边的子树对应的区间进行一次翻转
if (node.left != null) node.left.lazyReverse = !node.left.lazyReverse;
if (node.right != null) node.right.lazyReverse = !node.right.lazyReverse;
node.lazyReverse = false;//当前节点对应的区间完成翻转,去掉当前节点的懒标记
return node;
}
private ArrayList<TreapNode<T>> splitBySize(TreapNode<T> node, int size) {
//返回的数组中,第一个表示等于val的节点,第二个是剩余部分的树
ArrayList<TreapNode<T>> res = new ArrayList<>();
if (node == null) {
res.add(null);
res.add(null);
} else if (size >= root.size) {
res.add(node);
res.add(null);
} else if (size <= 0) {
res.add(null);
res.add(node);
} else {
if (node.lazyReverse) node = pushDown(node);//如果当前节点有懒标记,就先翻转一下
int leftSize = node.left != null ? node.left.size : 0;
if (leftSize < size) {
//如果左子树大小 < 输入的大小,说明当前的node肯定属于等于size大小部分的树,那node就作为最高的节点
//然后剩余的数量size-leftsize-1部分就到node的右节点去找
ArrayList<TreapNode<T>> tmp1 = splitBySize(node.right, size - leftSize - 1);
node.right = tmp1.get(0);
res.add(node);
res.add(tmp1.get(1));
} else {
//说明左子树大小 大于等于 输入的大小,那当前节点就应该属于剩余部分的树
ArrayList<TreapNode<T>> tmp2 = splitBySize(node.left, size);
node.left = tmp2.get(1);
res.add(tmp2.get(0));
res.add(node);
}
leftSize = node.left != null ? node.left.size : 0;
int rightSize = node.right != null ? node.right.size : 0;
node.size = leftSize + rightSize + 1;
}
return res;
}
private TreapNode<T> merge(TreapNode<T> node1, TreapNode<T> node2) {
//权值按照大顶堆
if (node1 == null) return node2;
else if (node2 == null) return node1;
else {
//这里要确保node1的值小于node2
if (node1.value.compareTo(node2.value) >= 0) {
TreapNode<T> tmp = node1;
node1 = node2;
node2 = tmp;
}
if (node1.weight > node2.weight) {
//node1的权重大于node2,那node2会成为node1的子节点,又node1的值小于node2的值,那node2就在node1的右子
//只要节点涉及到修改子节点,就要先进行一次下传
if (node1.lazyReverse) {
node1 = pushDown(node1);
}
node1.right = merge(node1.right, node2);
int leftSize = node1.left != null ? node1.left.size : 0;
int rightSize = node1.right != null ? node1.right.size : 0;
node1.size = leftSize + rightSize + 1;
return node1;
} else {
//node1的权重小于等于node2,那node1会成为node2的子节点,又node1的值小于node2的值,那node1就在node2的左子
//只要节点涉及到修改子节点,就要先进行一次下传
if (node2.lazyReverse) node2 = pushDown(node2);
node2.left = merge(node2.left, node1);
int leftSize = node2.left != null ? node2.left.size : 0;
int rightSize = node2.right != null ? node2.right.size : 0;
node2.size = leftSize + rightSize + 1;
return node2;
}
}
}
public void merge(T val) {
root = merge(root, new TreapNode<>(val));
}
private void get_In(TreapNode<T> node) {
if (node == null) return;
if (node.lazyReverse) node = pushDown(node);//输出的时候肯定就要完成全部翻转了
get_In(node.left);
System.out.print(node.value + " ");
get_In(node.right);
}
public void getIn() {
get_In(root);
}
public class TreapNode<T extends Comparable<T>> {
private T value;//节点所存储的值
private int weight;//节点的权值,到插入的时候再分配
private TreapNode<T> left;//左子节点
private TreapNode<T> right;//右子节点
private int size;//树的大小
private boolean lazyReverse;//这里相比于普通的fhqTreap就是多了个懒标记
private TreapNode(T value) {
this.value = value;
this.size = 1;
this.weight = random.nextInt(range + 1);//给新节点生成一个随机权值
this.lazyReverse = false;
}
}
}
public class test {
public static void main(String[] args) {
WenYiTree<Integer> fhq = new WenYiTree<>();
for(int i = 1; i <= 5; i++) {
fhq.merge(i);
}
fhq.getIn();
System.out.println();
fhq.reverse(2,6);
fhq.getIn();
}
}