平衡二叉搜索树(Binary Sort Tree)
一丶概念
平衡二叉树是一种特殊的二叉搜索树,关于二叉搜索树,请先了解之后再阅读该博文。那对于今天的平衡二叉搜索树它有什么特别的地方呢,了解过二叉搜索树的基本都清楚,在按顺序向插入二叉搜索树中插入值,最后会形成一个类似链表形式的树,而我们设计二叉搜索树的初衷,显然是看中了它的查找速度与它的高度成正比【O(logn)】,如果每一颗二叉树都像链表一样,那就没什么意思了。
【如果遇见最差的情况,比如以下这棵树:】
对于上面这样的情况,所以就设计出来了平衡二叉树,相对于二叉搜索树,平衡二叉树的一个特点就是,在该树中,任意一个节点,它的左右子树的差的绝对值一定小于。对于上面的最差情况的图而言,这棵树,说是树,其实它已经退化成链表了,但从概念上来看,它仍是一棵二叉搜索树,但是对于上面的这样的情况而言,导致插入的时间复杂度就变成了O(n),导致这种糟糕的情况原因是因为这棵树极其不平衡,右树的重量远大于左树,因此我们提出了叫平衡二叉搜索树的结构,又称之为AVL树。
在本文中,为了方便,也是采用了 int 型值插入。
二丶演示
假如上面的二叉搜索树用AVL树实现的话如下
AVL树的性质:
1. 左子树与右子树高度之差的绝对值不超过1
2. 它的左右子树都是AVL树
3.对于每一个节点都需要维持一个平衡因子(balance factor),任一节点的平衡因子是-1、0、1(每一个节点的平衡因子 = 右子树高度 - 左子树高度)
三丶平衡二叉搜索树节点的构建(多维护一个height属性)
它的类和二叉搜索树中节点的类类似,每一个节点需要多维护一个hight属性
class AVL_Node{
public AVL_Node left;
public AVL_Node right;
//增加一个父节点用来记录
public AVL_Node parent;
public int element;
//这里需要维护一个高度属性,默认为1 【即平衡因子】
public int height = 1;
public AVL_Node(int element){
this.element=element;
}
四丶添加
对于平衡二叉树的添加,先考虑什么情况下会打破平衡【即左右节点的高度差的绝对值大于1】,这里假设A树是一个已经平衡的一颗二叉平衡搜索树,但是现在需要往树中添加一个元素,有两种情况:(1)平衡没有打破;(2)平衡被打破。
这里分析一下问题:
1.平衡是怎么被打破的?即我们是如何判断平衡被打破的!
2.平衡被打破了,该如何调整?
3.从最坏的情况考虑【单链表】
对于上面的两种基本情况,可以发现最终导致25这个节点不平衡
对于上面的情况纵观全局,假如对一个树中添加一个节点
对上面的树中加入一个节点:45
对于上面的而言,对于节点:45 的添加,导致该树不再平衡【92节点不再平衡】。这里我们可以分析出,当我们拿到拿到刚插入的节点时,不断向上parent找,能找到不平衡节点,假如当我们平衡了92节点是否会使得整颗树都平衡呢?答案是肯定的!而且对于添加,我们可以知道有些情况当我们添加一个节点时会导致多个父节点失衡,但是我们只需要调整平衡最靠近新添加节点的父节点,就会恢复整颗树的平衡
这里又有问题:
1丶怎么找到最靠近新添加节点的失衡节点呢?
2丶怎么恢复该失衡节点呢
显然是对添加完节点后再拿到该节点对该节点进行向上查找失衡的父节点!【这里一定要记住,失衡节点一定有孩子,失衡节点的孩子也一定有孩子,这里的孩子可以是左孩子或者右孩子,】
对于常规的add方法这里一遍过,本文着重将afteradd()方法的实现,即对add()添加之后的调整,实现平衡
public void add(AVL_Node Node){
if(root==null){
root=Node;
root.parent=null;
addAfter(root); //add之后的平衡操作
return;
}
AVL_Node parent=root;
AVL_Node temp= root;
while (temp!=null){
parent=temp;
if(temp.element>Node.element){
temp=temp.left;
}
else if(temp.element<Node.element){
temp=temp.right;
}
else return;
}
if(parent.element>Node.element){
parent.left=Node;
}else {
parent.right=Node;
}
Node.parent=parent;
addAfter(Node); //add之后的平衡操作
}
为了辅助后续add()和remove()等方法,为节点添加各种辅助方法
public boolean has_twoChild(){
return this.left!=null&&this.right!=null;
}
public boolean isLeaf(){
return this.left==null && this.right==null;
}
//更新该节点的高度 【每添加一个节点更新一个该节点的高度】
public void Update_height(){
int left_height= left == null ? 0 : left.height;
int right_height= right == null ? 0 : right.height;
height = 1 + Math.max(left_height,right_height);
}
//平衡因子 【用来维持绝对平衡】
public int balanceFactor(){
int left_height= left == null ? 0 : left.height;
int right_height= right == null ? 0 : right.height;
return left_height - right_height;
}
//返回子节点中最高的那个节点
public AVL_Node tallerChild(){
int left_height=left == null ? 0 : left.height;
int right_height=right == null ? 0 : right.height;
if(left_height > right_height)return left;
if(left_height < right_height)return right;
//左右节点高度一样高的情况的处理//同方向返回
return this.isParent_Left() ? left : right;
}
//判断自己当前节点是父节点的那个子节点
public boolean isParent_Left(){
return this.parent!=null && this.parent.left==this;
}
//判断自己当前节点是父节点的那个子节点
public boolean isParent_Right(){
return this.parent!=null && this.parent.right==this;
}
五:找失衡节点
对于上面说的找失衡节点,我们在add()方法中可以看出,在添加完节点后,将新添加的节点传入到addAfter()方法中,这里的代码块为直接上来就是拿到该节点的父节点,不断向上查找,思路是假如该节点为平衡,即更新高度,反之恢复平衡,恢复平衡后,直接 break,因为不再需要向上查找,整颗树已经平衡【注意:这里的release_balance()方法中也有更新高度的方法实现,所以不用担心失衡节点上面的代码没更新高度】
public void addAfter(AVL_Node node){
/**该addAfter()函数作用:=----> 对新添加的节点的父节点进行判断,判断是否平衡/是否不平衡
* 为什么需要遍历新添加的节点的所有父节点:=----->
* 起初while循环是为了找到第一个不平衡节点的,让其平衡,
* 【最终发现可以利用while来进行更新高度的操作】
*/
while ((node=node.parent)!=null){ //迭代求法:不需要递归求高度,just在迭代里面求得高度即可
if(isBalanced(node)){
update_height(node); //不断向上找父节点更新高度
}else {
release_balance(node); //传进的是不平衡的 “节点(父节点)”
break;
}
}
}
【 这里轴重强调afteradd()方法中的调用Update_height()方法的思想,对于正常的求高度的方法我们可以使用递归求解高度,但是考虑到性能的问题,这里我们可以在afteradd()方法中的不断向上查找不平衡节点的同时,迭代更新高度,即每向上查找不平衡节点的同时,向上更新高度时+1 】
六:调整【左旋转,右旋转】
对于上面找到了失衡节点以后,怎么调整让其平衡呢?
对于上面所说的,失衡节点一定有孩子,孩子的孩子一定也有孩子,大致可以分为下面情况【这里我只分析了失衡节点的孩子为左节点的情况,假如为右节点同理也是两种情况,即 LL / LR, RR / RL 一共四种情况】
所谓右旋转:【这里以25为例】{把12和25看出一根棍子,右旋转后:25下,12上}
右旋转后,如图所示:
由上图可知,右旋转后,原本节点:25失衡,现在全部节点不再失衡,而且依然符合二叉搜索树的性质,即 9 > 12 > 25
这里我们讨论完失衡节点的孩子节点和失衡节点的孩子的孩子在一个方向的时候【LL / RR】,还有就是【LR / RL】情况
我这里讨论 LR 情况【RL类似】
对于下面的情况,先对12节点(失衡节点的孩子节点)左旋转,即12下去,20上来【这里有交换对应指针指向】
得到如下情况,是不是很熟悉呢!没错,就是 LL,再对失衡节点进行右旋转即可恢复平衡
综上所示:
由以上可知,大致有以下四种操作情况
一、只需要经过一次右旋即可达到平衡
二、只需要经过一次左旋即可达到平衡
三、需先经过左旋,再经过右旋也可达到平衡
四、需先经过右旋,再经过左旋也可达到平衡
这里直接上代码:
public void release_balance(AVL_Node grand){
/**
* 传进的是最低的不平衡节点
* 找到LL/LR/RR/RL
* parent 和 child 都一定存在!
*/
AVL_Node parent = grand.tallerChild();
AVL_Node child = parent.tallerChild();
if(parent.isParent_Left()){
if(child.isParent_Left()){ //LL
while_right(grand);
}else {
while_left(parent);
while_right(grand);
}
}
else if(parent.isParent_Right()){
if(child.isParent_Right()){ //RR
while_left(grand);
}else {
while_right(parent);
while_left(grand);
}
}
}
//左旋转(RR)
public void while_left(AVL_Node grand){ //grand节点 【三者都需要更新父节点】
AVL_Node parent = grand.right; //parent节点
AVL_Node child = parent.left; //child节点
//交换两个指针指向
grand.right=child;
parent.left=grand;
//维护父节点1
parent.parent=grand.parent;
if(grand.isParent_Right()){
grand.parent.right=parent; //可能grand.parent为null
}
else if(grand.isParent_Left()){
grand.parent.left=parent;
}else { //处理grand.parent为null情况,即grand为root
root=parent;
root.parent=null;
}
if(child!=null){child.parent=grand;} //维护父节点2
grand.parent=parent; //维护父节点3
update_height(grand);
update_height(parent);
}
//右旋转(LL)
public void while_right(AVL_Node grand){
AVL_Node parent = grand.left;
AVL_Node child = parent.right; //可能为空null
grand.left=child;
parent.right=grand;
parent.parent=grand.parent;
if(grand.isParent_Left()){
grand.parent.left=parent;
}else if(grand.isParent_Right()){
grand.parent.right=parent;
}else {
root=parent;
}
grand.parent=parent;
if(child!=null){
child.parent=grand;
}
/**
* 先 “update” 更新低的节点的高度,再更新高节点的高度
*/
update_height(grand);
update_height(parent);
}
//判断平衡
public boolean isBalanced(AVL_Node node){
return Math.abs(node.balanceFactor()) <=1;
}
//更新高度
public void update_height(AVL_Node node){
node.Update_height();
}
removeAfter()方法
待续……