定义
一棵AVL树可以是空树,也可以是具有下列性质的二叉搜索树:它的左子树和右子树都是AVL树,且左子树和右子树的高度之差的绝对值不超过1(也就是小于等于1)。(右子树高度 - 左子树高度)
节点的平衡因子:给每个节点附加一个数字,给出该节点右子树的高度减去左子树的高度所得的高度差,这个数字即为节点的平衡因子。
如果一棵二叉搜索树的高度是平衡的,它就成为了AVL树,如果它有n个节点,其高度可保持在O(log2n)。
AVL树的旋转
平衡化旋转:
① 如果在一棵平衡的二叉搜索树中插入一个新节点,造成了不平衡。此时必须调整树的结构,使之平衡化。
② 平衡化旋转有两类:单旋转和双旋转。
其中单旋转又可分为左单旋转和右单旋转。
每插入一个新节点时,AVL树中相关的平衡状态会发生改变,因此,在插入一个新节点后,需要从插入的位置沿向根的路径回溯,检查各节点的平衡因子。
如果在某一节点发现高度不平衡,停止回溯,从发生不平衡的节点起,沿刚才回溯的路径直接下两层的节点,如果这三个节点处于一条直线上,则采用单旋进行平衡化。单旋转可按其旋转方向分为左单旋转和右单旋转,其中一个是另一个的镜像,其旋转方向与不平衡的形状有关。
如果这三个节点处于一条折线上,则采用双旋转进行平衡化。双旋转分为先左后右和先右后左两类。
左单旋转
如图,此时这棵树是一根平衡的二叉搜索树,所以我们也可以称这棵树为AVL树。每个节点的平衡因子用红色字体标出。我们可以看到,每个节点的平衡因子的绝对值都没有超过1.此时该树平衡。
此时,我们插入新的节点,节点值为85,我们发现,50这个节点的平衡因子超过1了。这棵树已经是不平衡的了,如下图所示:
根据我们的旋转规则:从新加入的节点开始沿根的路径回溯,找到不平衡的节点,从这个不平衡的节点开始沿刚才回溯的路径向下寻找两层节点,如果这三个节点在同一直线上,我们就是用单旋转,否则使用双旋转。依据这个规则,我们找到了不平衡的节点50,向下两层找到了节点70、节点80。这三个节点在同一直线上,所以我们采用单旋转。
以不平衡节点的下一个节点为旋转点,进行左单旋转:如图所示:
如上图所示,我们首先定义一个ptr
引用指向不平衡的节点,然后它的右孩子,是我们旋转后的根节点,我们将它定义为newRoot
具体是怎么旋转的,我们来看看下图:
具体代码实现:
// AVL树的结构与二叉搜索树的结构相似,只是多了一个 int balance遍历表示平衡因子。
// 这里AVL树的结构就不再给出。直接给出坐旋转的代码。
public void rotateLeft(ANode ptr){
ANode newRoot = ptr.rightChild; // ①
newRoot.parent = ptr.parent; // 维护newRoot的双亲。
// 维护newRoot.leftChild的双亲。
if (newRoot.leftChild!=null) {
newRoot.leftChild.parent = ptr;
}
ptr.rightChild = newRoot.leftChild;//③
newRoot.leftChild = ptr; // ②
ANode pa = ptr.parent;
// pa == null 说明旋转前,ptr就是根节点
if (pa==null){
root = newRoot;
}else{
// pa != null 说明ptr要么是上一棵AVL树的左子树
// 要么是上一棵AVL树的右子树。
if (pa.leftChild == ptr){
pa.leftChild = newRoot;
}
if (pa.rightChild == ptr){
pa.rightChild = newRoot;
}
}
//维护ptr的双亲
ptr.parent = newRoot;
}
右单旋转
右单旋转和左单旋转的思路是一样的,只不过是旋转的方向不同,我们还是用图示来说明。
如图,此时是一棵平衡的AVL树。
当我们向这棵AVL树中添加 节点10时,如下图所示:
此时,我们从新插入的节点10开始,沿着根路径向上回溯的查找,直到查找到80这个节点,发现该节点的平衡因子的绝对值大于1,于是,从该节点向下,沿着刚才回溯的路径,找到下两层节点40、30。这三个节点在同一条直线上,此时以40这个节点为旋转点,进行右单旋转。
如下图:
代码实现:
// AVL树右单旋转
public void rotateRight(ANode ptr){
ANode newRoot = ptr.leftChild;
newRoot.parent = ptr.parent; //维护newRoot的双亲
//维护newRoot.rightChild的双亲
if (newRoot.rightChild!=null){
newRoot.rightChild.parent = ptr;
}
ptr.leftChild = newRoot.rightChild;
newRoot.rightChild = ptr;
ANode pa = ptr.parent;
if (pa == null){
root = newRoot;
}else{
if (pa.rightChild == ptr){
pa.rightChild = newRoot;
}
if(pa.leftChild ==ptr){
pa.leftChild = newRoot;
}
}
ptr.parent = newRoot; //维护ptr的双亲
}
双旋转
若某一个节点的平衡因子发生改变,沿着回溯路径,以最后一层的节点为旋转点,进行先左单旋后右单旋,或者进行先右单旋后左单旋。
现有一棵AVL树,每个节点的平衡因子的绝对值 都没有超过1,所以该AVL是平衡的。
此时,我们新节点插入的位置有四种,分别为上述图中标出来的1、2、3、4这四个位置。
情况一:
首先分析,如果我们插入的位置为1或者2这两个位置其中之一,则有:
此时,我们发现无论我们插入的是1位置 还是 2位置,节点B的平衡因子都会变成-1,而节点A的平衡因子都会变成-2。都会导致AVL树不平衡,需要旋转。
我们从新插入的节点开始,向上回溯至不平衡节点的位置,找到了A节点,于是按照旋转规则:找到刚才回溯路径包括A节点在内的3个节点,即A、B、D开始进行旋转。 即,如果我们新的节点插入到1或者2这两个位置其中之一,我们需要旋转的条件就是:当B的平衡因子变为-1时,我们就要进行单旋转,再进行完单旋转后,我们还需要对B的平衡因子重新赋值。
我们上面分析过了,无论插入1还是2位置,只是进行单旋转,如下图:
我们发现了一个有意思的事情,无论新插入的节点插入1位置还是2位置,在旋转前,B的平衡因子都会变为-1,而在旋转后,B的平衡因子会变为0,A的平衡因子也会变为0。声明一点:图中没有标注ptr
和newRoot
两个引用,ptr
和newRoot
的指向和右单旋转是一样的。
情况二:
当我们新节点插入的是3位置
此时,B的平衡因子变为1,E的平衡因子变为-1。从新插入节点F处开始向上查找不平衡的节点,找到了A,于是按照遍历规则,A及其下两层的节点要进行旋转。由于A、B、E这三个节点不在一条直线上,所以我们需要双旋转。先以E节点为旋转点,EB进行左单旋转。如图所示:
接着,再以E为旋转带你,EA为轴进行右单旋转,如图:
此时,我们发现,E的平衡因子变为0,A的平衡因子变为1。
整体旋转过程如图所示:
情况三:
当新节点插入的位置在4号位置时,如图所示:此时,B的平衡因子还是变为1,E的平衡因子变为了 1 。
于是,我们接着按照旋转规则,从新插入节点F处向上查找不平衡的节点,找到A节点,然后A节点向下两层找到A、B、E三个节点,这三个节点不在一条直线上,我们需要进行双旋转。
首先,以E为旋转点,EB进行左单旋,如图所示:
再以E为旋转点,EA进行右单旋,如图:此时我们发现E的平衡因子为0,A的平衡因子为0,B的平衡因子为-1。
整体流程如图所示:
至此,我们分析完了所有情况,我们回顾一下,三种不同情况各节点的平衡因子变化情况:
情况一:新节点插入1位置
情况一:新节点插入2位置
我们发现,节点A和节点B的平衡因子都变为了0,其余节点的平衡因子都没有发生改变。
我们再来看看情况二,新节点插入3位置:我们发现,A、B、E三个节点的平衡因子都发生了改变。
我们再来看看情况三,新节点插入4位置:我们发现,同样的是A、B、E三个节点的平衡因子发生改变。其余节点的平衡因子没有发生改变。
代码实现:
// AVL树的左平衡
public void leftBalance(ANode ptr){
ANode leftSub = ptr.leftChild;
ANode rightSub = null;
switch(leftSub.balance){
// 说明新插入节点的位置在1位置或2位置
case -1:
leftSub.balance = 0;
ptr.balance = 0;
rotateRight(ptr);
break;
case 0:
System.out.println("the tree is balance");
break;
// 说明新插入的节点的位置在3位置或4位置,具体情况还要分析
case 1:
rightSub = leftSub.rightChild;
switch (rightSub.balance){
// 说明插入的是3位置
case -1:
ptr.balance = 1;
leftSub.balance = 0;
// 说明插入的是4位置
case 1:
ptr.balance = 0;
leftSub.balance = -1;
case 0:
ptr.balance = 0;
leftSub.balance = 0;
}
rightSub.balance = 0;
rotateLeft(leftSub);
rotateRight(ptr);
break;
}
}
以上就是我们的双旋转的一种情况:先左单旋转后右单旋转,第二种情况:先右单旋转后左单旋转这里就不再做演示了。和先左后右旋转是一样的思路。如过程中有不对的地方,希望各位能够指点,谢谢!