一、概念
在实现二叉树之后,我们发现在某些极端的情况下,二叉树可能会发生退化,退化成复杂度为O(N)的链表,为了避免这种情况,我们必须要给二叉树添加限定条件。AVL树是一种带有平衡条件的二叉查找树,由Adelson-Velskii和Landis于年首先提出。使二叉查找树成为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树要多出高度这个属性。
privatestaticclassAvlNodeT{Telement;AvlNodeTleft;AvlNodeTright;intheight;AvlNode(Telement){this.element=element;this.left=null;this.right=null;this.height=0;}}
在定义好节点之后,我们根据上面分析的策略,实现单旋转,从上面的分析很容易看出,单旋转有两种:左旋转和右旋转。
//左旋转,传入要平衡的节点aprivateAvlNodeTrotateWithLeftChild(AvlNodeTa){AvlNodeTk=a.left;a.left=k.right;k.right=a;//节点位置改变以后要同时改变相应节点的高度k.height=a.height;a.height--;returnk;}//右旋转,传入要平衡的节点aprivateAvlNodeTrotateWithRightChild(AvlNodeTa){AvlNodeTk=a.right;a.right=k.left;k.left=a;//节点位置改变以后要同时改变相应节点的高度k.height=a.height;a.height--;returnk;}//返回节点的高度,空树的高度为-1privateintheight(AvlNodeTa){returna==null?-1:a.height;}单旋转完成以后,我们开始完成双旋转,拿情况3来说,该操作应当是节点6先进行一次右旋转变成情况1,再进行一次左旋转完成平衡。
privateAvlNodeTdoubleWithLeftChild(AvlNodeTa){a.left=rotateWithRightChild(a.left);returnrotateWithLeftChild(a);}privateAvlNodeTdoubleWithRightChild(AvlNodeTa){a.right=rotateWithLeftChild(a.right);returnrotateWithRightChild(a);}单旋转和双旋转完成以后,接下来我们就要添加平衡的方法了,从AVL树的定义我们可以知道,如果节点a的左子树和右子树的高度差大于1,那么我们就应当触发单旋转或者双旋转来保持树的平衡
privateAvlNodeTbalance(AvlNodeTt){if(t==null){returnnull;}if(height(t.left)-height(t.right)1){if(height(t.left.left)=height(t.left.right)){//情况1t=rotateWithLeftChild(t);}else{//情况3t=doubleWithLeftChild(t);}}if(height(t.right)-height(t.left)1){if(height(t.right.right)=height(t.right.left)){//情况2t=rotateWithRightChild(t);}else{//情况4t=doubleWithRightChild(t);}}//更新t节点的高度t.height=Math.max(height(t.left),height(t.right))+1;returnt;}按照当前的理论,二叉树的平衡条件已经实现,接下来就是在我们之前实现的二叉查找树的代码里面添加这些方法,并修改insert和remove方法,将这两个的方法的返回值改为returnbalance(t)即可。四:测试验证
在之前实现二叉树后,我们逐一插入A、B、C、D、E进行测试,发现二叉树退化成了链表,现在我们添加平衡的方法后,再逐一插入A、B、C、D、E进行测试
TestpublicvoidAvlTreeTest(){AvlTreeStringt=newAvlTree();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树代码的朋友可以