一、概念
在实现二叉树之后,我们发现在某些极端的情况下,二叉树可能会发生退化,退化成复杂度为O(N) 的链表,为了避免这种情况,我们必须要给二叉树添加限定条件。
AVL树是一种带有平衡条件的二叉查找树,由Adelson-Velskii 和Landis于1962年首先提出。使二叉查找树成为AVL树的性质是:二叉查找树的每个节点的左子树和右子树的高度最多差1(空树的高度定义为-1)
如上图所示,左边的是AVL树,右边的就不是,因为其左子树和右子树的高度差为2。
二、平衡策略
要想让AVL始终保持平衡,我们需要一定的策略。
如上图左侧的AVL树,如果我们向其插入6,就会破坏AVL树的平衡,因为在8处的左子树高度为2,右子树高度为0。为了让右侧的树保持平衡,我们很容易想到可以像如下图那样旋转一次,使其重新保持平衡。
为了尽可能的减少节点的移动(提升效率),通过分析我们可以知道:因插入节点而破坏AVL树平衡的情况有四种。
假设我们将需要平衡的节点称为a,那么总共就是以下这四种情况
1、a节点左子树的左子节点(如图1)。
2、a节点右子树的右子节点(如图2)。
3、a节点左子树的右子节点(如图3)。
4、a节点右子树的左子节点(如图4)。
上图中要平衡的a节点 就是图中的节点8,其中第1、2种情况是关于a节点镜像对称;第3、4种情况同样也是关于a节点镜像对称。1、2的情况下,为了保持平衡只需要单旋转一次即可;3、4的情况稍复杂点,需要经过双旋转才能保持平衡。(第3种情况第一次旋转变成第1种情况,第二次旋转就可以保证平衡)
新增的情况分析完了,那么删除呢?删除和新增情况基本类似。
如上图,将左侧的AVL树的节点10 删除,这便破坏了树的平衡,我们仔细看下删除10 以后的树结构,这和上面情况1结构很类似,只不过,删除情况下节点7 的左右子节点都是齐全的,而在情况1中节点7 只有左子节点,于是我们在进行单旋转的时候要考虑到节点8 应该放的位置,如上图节点8 应该放在节点9 的左子节点位置。
三、代码实现
经过以上的分析,下面我们开始手动代码实现:
首先我们先定义出AVL树的节点,AVL树的节点定义和二叉树节点定义很类似,只不过AVL树要多出高度这个属性。
private static class AvlNode<T>{
T element;
AvlNode<T> left;
AvlNode<T> right;
int height;
AvlNode(T element){
this.element= element;
this.left = null;
this.right = null;
this.height = 0;
}
}
在定义好节点之后,我们根据上面分析的策略,实现单旋转,从上面的分析很容易看出,单旋转有两种:左旋转和右旋转。
// 左旋转,传入要平衡的节点a
private AvlNode<T> rotateWithLeftChild(AvlNode<T> a){
AvlNode<T> k = a.left;
a.left = k.right;
k.right = a;
//节点位置改变以后要同时改变相应节点的高度
k.height = a.height;
a.height--;
return k;
}
// 右旋转,传入要平衡的节点a
private AvlNode<T> rotateWithRightChild(AvlNode<T> a){
AvlNode<T> k = a.right;
a.right= k.left;
k.left = a;
//节点位置改变以后要同时改变相应节点的高度
k.height = a.height;
a.height--;
return k;
}
// 返回节点的高度,空树的高度为-1
private int height(AvlNode<T> a){
return a == null ? -1 : a.height;
}
单旋转完成以后,我们开始完成双旋转,拿情况3来说,该操作应当是节点6先进行一次右旋转变成情况1,再进行一次左旋转完成平衡。
private AvlNode<T> doubleWithLeftChild(AvlNode<T> a) {
a.left = rotateWithRightChild(a.left);
return rotateWithLeftChild(a);
}
private AvlNode<T> doubleWithRightChild(AvlNode<T> a) {
a.right= rotateWithLeftChild(a.right);
return rotateWithRightChild(a);
}
单旋转和双旋转完成以后,接下来我们就要添加平衡的方法了,从AVL树的定义我们可以知道,如果节点a的左子树和右子树的高度差大于1,那么我们就应当触发单旋转或者双旋转来保持树的平衡
private AvlNode<T> balance(AvlNode<T> t) {
if(t == null){
return null;
}
if(height(t.left) - height(t.right) > 1){
if( height(t.left.left) >= height(t.left.right) ){
// 情况1
t = rotateWithLeftChild(t);
}else{
// 情况3
t = rotateWithLeftChild(t);
}
}
if(height(t.right) - height(t.left) > 1){
if(height(t.right.right) >= height(t.right.left)){
// 情况2
t = rotateWithRightChild(t);
}else{
// 情况4
t = doubleWithRightChild(t);
}
}
// 更新t节点的高度
t.height = Math.max(height(t.left), height(t.right)) + 1;
return t;
}
按照当前的理论,二叉树的平衡条件已经实现,接下来就是在我们之前实现的二叉查找树 的代码里面添加这些方法,并修改insert和remove方法,将这两个的方法的返回值改为 return balance(t) 即可。
四:测试验证
在之前实现二叉树后,我们逐一插入A、B、C、D、E进行测试,发现二叉树退化成了链表,现在我们添加平衡的方法后,再逐一插入A、B、C、D、E进行测试
@Test
public void AvlTreeTest() {
AvlTree<String> t = new AvlTree<>();
t.insert("A");
t.insert("B");
t.insert("C");
t.insert("D");
t.insert("E");
t.printTree();
}
结果如下图展示
情况符合我们的预期,至此,我们的AVL树已经成功实现。
关于AVL树的实际应用,我们很容易就能想到其在查找方面具有很高的效率,因为整棵树的结构都是有序的,当查找一个值的时候,将该值和根节点的值对比,就能够确定要查找的值是在左子树下面还是在右子树下面,理论上其操作时间为O(logN) ,而链表的操作时间为O(N),随着数据量的增多,AVL树操作的效率提升越明显。
需要AVL树代码的朋友可以 关注本公众号,回复:AVL树代码 即可获取完整代码
本人因所学有限,如有错误之处,望请各位指正!