竞赛最好用的平衡树-Size Balanced Tree(SBT)建议收藏
Posted 飞人01_01
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了竞赛最好用的平衡树-Size Balanced Tree(SBT)建议收藏相关的知识,希望对你有一定的参考价值。
大家好。
前段时间我更新过AVL、红黑树以及搜索二叉树的简单讲解。今天我们还是围绕着平衡树的话题,来讲解一个很牛逼的平衡树结构。这种结构是我国的一位大牛:陈启峰。在2006年,他还在读高中阶段,所发明的一种平衡树结构,叫Size Balanced Tree(SBT),根据节点的个数,来调整平衡性。一直到今天,这种平衡树结构,在算法竞赛领域是非常常用的,虽然SBT的时间复杂度跟AVL、红黑树这些平衡树一样,但是SBT是比较好写,比较好改的。所以在算法竞赛时,是最常用的一种算法。陈启峰SBT论文(译)
本期文章源码:GitHub
前期文章:
一、左右旋转
还是老样子,为了维持平衡性,SBT也是需要进行旋转操作的。只是说,调用旋转操作的时机,跟其他的平衡树有点区别而已。讲解旋转之前,我们先来认识SBT的节点:
//可以直接改为泛型,这里为了理解这种结构,就忽略了
public class SBTNode {
public int val; //值域
public SBTNode left, right; //左右孩子
public int size; //节点数
public SBTNode(int val) {
this.val = val;
size = 1; //默认节点大小是1
}
}
SBT的节点,只是多了一个size域,用于表示以当前节点为根节点时,这颗子树的节点数。整棵树的平衡性,就是根据这个size来调整的。
右旋转:(LL型)
旋转操作的指针,相互之间的转换,我相信以前了解过平衡树的同学,应该是知道的(如果不知道,请翻阅前期文章)。问题就在于如何,如何修改这些节点的size?
看上图,T1-T3,表示子树,另外两个表示节点,旋转之后,我们会发现,T1和T2和T3这三颗子树,他们下面的节点是没有发生改变的,也就是说T1-T3,这三个是不需要再次计算size的,我们只需要计算上图那两个白色的节点的size即可!
又因为,旋转之后,新的根节点的节点数,还应该是原根节点的节点数,所以最后我们只需要计算旋转之后,原根节点的节点数即可,也就等于该节点的左子树节点数加上右子树的节点数,再加自己本身这个节点。
//SBT右旋转
private SBTNode R_Rotate(SBTNode node) {
SBTNode newRoot = node.left;
node.left = newRoot.right;
newRoot.right = node;
//计算size
newRoot.size = node.size; //新根节点的节点数,等于原根节点的节点数
node.size = (node.left != null? node.left.size : 0) +
(node.right != null? node.right.size : 0) + 1;
return newRoot; //返回新的根节点
}
左旋转:(RR型)
跟上面的旋转一样,每个节点之间的指针指向,这里就不深究了,同学可以看看我前期的文章,有讲解。主要还是size的计算,同样的,T1~T3的节点数,都是没有变,所以不用管。只需计算原根节点和新根节点的节点数,切新根节点的节点数,就是原根节点的节点数。
//SBT左旋转操作
private SBTNode L_Rotate(SBTNode node) {
SBTNode newRoot = node.right;
node.right = newRoot.left;
newRoot.left = node;
//计算size
newRoot.size = node.size; //继承原根节点的节点数
node.size = (node.left != null? node.left.size : 0) +
(node.right != null? node.right.size : 0) + 1;
return newRoot; //返回新的根节点
}
以上两种就是最基本的LL型和RR型,在SBT中,也是有LR型和RL型的,跟AVL中一样,不需要再写额外的代码,只需要调用两次左旋转或者右旋转。如下:
- 假设A节点需要进行LR型的旋转,则只需A.left = 左旋转一次,再A= 右旋转一次。
- 同理,假设B节点需要进行RL型的旋转,则只需B.right = 右旋转一次,再B = 左旋转一次。
具体在什么时候需要调用旋转函数,我们在接下来的Maintain方法里讲解。
二、Maintain方法
Maintain方法,就是SBT树,最核心的地方,也就是这个方法,能够保证一棵树具有平衡性的。
首先,我们需要知道,在什么时候,才需要进行旋转操作。在AVL中,是通过判断平衡因子来处理平衡性;在左倾红黑树中,则是根据每个节点颜色来处理平衡性。而SBT是:
首先看这张图:
上面的四种旋转(LL、LR、RR、RL),分别对于一下四点:
- LL型:T1的size > 二叔的size
- LR型:T2的size > 二叔的size
- RL型:T3的size > 大叔的size
- RR型:T4的size > 大叔的size
以上四点,就是触发机关,只要满足这四点的某一个条件,则需要进行旋转操作。总结起来就一句话:叔叔的节点数,必须大于等于侄子的节点数。不然的话,就需要进行平衡调整。具体是为什么这种机制,就能够保证平衡性,请翻阅文章开头,陈启峰的那篇论文。
到目前为止,我们就知道了SBT的核心,代码写起来就简单多了,分别计算叔叔节点和侄子节点的节点数,if判断即可。值得注意的是,旋转操作之后,还需要递归调用Maintain方法。递归调用的对象,就是:哪个节点的子树被换了,则需要调用这个Maintain(新子树);举个例子:原先A节点的右子树是T2,旋转操作之后,A节点的右子树变为了T3,那么就需要递归调用Maintain(T3)。
//Maintain方法
private SBTNode maintain(SBTNode cur) {
if (cur == null) {
return null;
}
//计算对应节点的节点数,null的话,就是0
int leftSize = cur.left != null ? cur.left.size : 0;
int rightSize = cur.right != null ? cur.right.size : 0;
int leftLeftSize = cur.left != null ? (cur.left.left != null ? cur.left.left.size : 0) : 0;
int leftRightSize = cur.left != null ? (cur.left.right != null ? cur.left.right.size : 0) : 0;
int rightLeftSize = cur.right != null ? (cur.right.left != null ? cur.right.left.size : 0) : 0;
int rightRightSize = cur.right != null ? (cur.right.right != null ? cur.right.right.size : 0) : 0;
if (leftLeftSize > rightSize) { //LL型
cur = R_Rotate(cur);//右旋
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (leftRightSize > rightSize) { //LR型
cur.left = L_Rotate(cur.left);
cur = R_Rotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (rightLeftSize > leftSize) { //RL型
cur.right = R_Rotate(cur.right);
cur = L_Rotate(cur);
cur.left = maintain(cur.left);
cur.right = maintain(cur.right);
cur = maintain(cur);
} else if (rightRightSize > leftSize) { //RR型
cur = L_Rotate(cur);
cur.left = maintain(cur.left);
cur = maintain(cur);
}
return cur;
}
切记:递归调用Maintain时,一定是先调用cur的左右子树,然后才是调用cur。因为cur的处理,是依赖于他的左右孩子的。
可能有同学就会疑惑了,这么多递归函数,这个代码能跑完吗?
当然能够跑完,因为旋转操作之后,递归调用Maintain,能够在O(1)的时间内完成。
三、add方法和delete方法
Maintain方法之后,SBT的就算掌握大部分的代码了,其余的添加和删除代码,完全跟搜索二叉树的增加删除,一模一样。只是需要在添加删除之后,调用Maintain方法,用于调整平衡性即可。
//add方法
public void add(int val) {
this.root = add(this.root, val);
}
//方法重载
private SBTNode add(SBTNode cur, int val) {
if (cur == null) {
return new SBTNode(val);
} else {
cur.size++; //沿途节点的节点数加1
if (val < cur.val) {
cur.left = add(cur.left, val);
} else {
cur.right = add(cur.right, val);
}
}
//添加之后,需要调用maintain,进行平衡操作
return maintain(cur);
}
在SBT中,delete的时候,在大多时候,是不需要进行平衡调整的。why?
是因为没必要。假设当前SBT树的高度是H,现在删除一个节点后,高度可能还是H,又或者是H- 1。此时调整平衡与不调整平衡,都不影响后续的操作。比如删除之后,高度还是H,下次查找或者添加时,时间复杂度还是O(H),影响因素还是高度值。所以在一些比赛中,为了达到优化,在delete中,不进行平衡性的调整。而是把平衡性的调整,放在了add方法里。
//delete方法
public void delete(int val) {
if (contains(val)) { //包含当前值得话,就递归删除
root = delete(root, val);
}
}
private SBTNode delete(SBTNode cur, int val) {
cur.size--; //沿途节点的节点数-1
if (cur.val > val) {
cur.left = delete(cur.left, val);
} else if (cur.val < val) {
cur.right = delete(cur.right, val);
} else {
if (cur.left == null && cur.right == null) { //没有左右子树
cur = null;
} else if (cur.left == null && cur.right != null) { //只有右子树
cur = cur.right;
} else if (cur.left != null && cur.right == null) { //只有左子树
cur = cur.left;
} else {
//左右两个子树都有的情况
SBTNode pre = null;
SBTNode des = cur.right; //向右子树查找最左节点
des.size--;
while (des.left != null) {
pre = des;
des = des.left;
des.size--; //最终的des节点,会重新计算节点数
}
if (pre != null) {
pre.left = des.right;
des.right = cur.right; //将des节点,替换cur节点
}
des.left = cur.left;//连接原先的左子树
des.size = des.left.size + (des.right != null ? des.right.size : 0) + 1;
cur = des;
}
}
//cur = maintain(cur); //平衡调整
return cur;
}
特别需要注意的是,就是被删除节点的左右子树都不为null时,需要找一个节点来替换当前被删除的节点。一般都是在被删除节点的右子树上,查找最小(最左)的节点进行替换。这一点,也是搜索二叉树的删除操作,最容易出错的一个点,值得重点关注。
还有一些简单的方法没写,大家自主实现即可。比如contains、isEmpty等等。
好啦。本期更新就到此结束啦!SBT树学好之后,可以帮助你在一些算法题上得到更好的帮助,这种结构也算比较好改。可能根据SBT改其他的结构。总之,这种结构,值得好好学习。
我们下期见吧!!!
以上是关于竞赛最好用的平衡树-Size Balanced Tree(SBT)建议收藏的主要内容,如果未能解决你的问题,请参考以下文章
十五、平衡二叉搜索树(Balanced Binary Search Tree)