目录
一、AVL树的概念
AVL树本质上是一棵二叉搜索树,即它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树。另外,AVL树每个结点的左右子树的高度之差,即平衡因子的绝对值不超过1。
二、平衡调整
首先,为什么AVL树需要有“自平衡”的功能?其实从AVL树的特点就可以看出来,因为AVL树每个结点的平衡因子的绝对值不超过1,所以在插入结点和删除结点后,要检查整棵树是否仍然平衡,如果不是则进行调整,使其恢复平衡。可以用以下Java代码来表示AVL树的结点:
public class AVLNode {
public int key; // 结点的键,用于标识结点在AVL树中的位置
public Object value; // 结点的值,用于存储数据
public int height; // 结点的高度
public AVLNode left; // 结点的左指针
public AVLNode right; // 结点的右指针
public AVLNode(int key, Object value) {
this.key = key;
this.value = value;
height = 1;
}
}
其中,结点高度(height)不是固定的,会随着结点数量的变化而变化,以下是更新结点高度的代码:
private void resetHeight(AVLNode avlNode) {
avlNode.height = Math.max(
avlNode.left == null ? 0 : avlNode.left.height,
avlNode.right == null ? 0 : avlNode.right.height
) + 1;
}
1、自平衡原理
下面用两个例子来探讨自平衡的实现原理。
例子一:
图2.1.1
如图2.1.1所示,可以看出结点1的平衡因子为-2,整棵树处于不平衡状态,如果想让其恢复平衡,需要结点1的右子树“让”一些结点给左子树,因此,不难看出,可以对结点1进行左旋转来达到目的,以下是左旋转步骤:
private AVLNode leftRevolve(AVLNode avlNode) {
// 先保存avlNode的右子树
AVLNode right = avlNode.right;
// avlNode的右指针指向right的左子树
avlNode.right = right.left;
// right的左指针指向avlNode
right.left = avlNode;
// 先更新avlNode的高度
resetHeight(avlNode);
// 再更新right的高度
resetHeight(right);
// 返回right作为调整后的root
return right;
}
经过一次左旋转之后,整棵树变为:
图2.1.2
要注意的是,在旋转完成后,必须先更新结点1的高度,再更新结点3的高度,因为结点3的高度来自于结点1的高度和结点4的高度,而结点4没有参与旋转,因此不需要更新其高度。
例子二:
图2.1.3
如图2.1.3所示,与例子1的区别是,结点3的左子树多了结点2。此时结点1的平衡因子为-2,同例子1,也要进行左旋转来达到平衡,以下是旋转结果:
图2.1.4
可以看出,与例子1的旋转结果几乎一致,只不过结点1的右子树多了结点2,这是这行代码完成的:
// avlNode的右指针指向right的左子树
avlNode.right = right.left;
右旋转同理,不再赘述,以下是右旋转的代码:
private AVLNode rightRevolve(AVLNode avlNode) {
// 保存avlNode的左子树
AVLNode left = avlNode.left;
// avlNode的左指针指向left的右子树
avlNode.left = left.right;
// left的右指针指向avlNode
left.right = avlNode;
// 先更新avlNode的高度
resetHeight(avlNode);
// 再更新left的高度
resetHeight(left);
// 返回left作为调整后的root
return left;
}
下面,开始讨论复杂一点的情况,以下图示中以"key-height"的形式展示每个结点。
2、LL旋转
图2.2.1
如图2.2.1所示,可以看出结点4的平衡因子为-3,为不平衡状态,而除了结点4以外,其它所有结点都处于平衡状态,因此,只需要对结点4进行左旋转,以下是旋转结果:
图2.2.2
可以看出,整棵树已经平衡,实际上只经历了一次左旋转,因此,不要被LL的字面含义所误导。
3、RL旋转
图2.3.1
如图2.2.3所示,同样只有结点4不平衡(平衡因子-3),而如果仍然按照LL旋转中的方法,只对结点4进行一次左旋转,会发生什么?
图2.3.2
可以看到,不仅结点4没有恢复平衡(平衡因子-2),还多了一个不平衡的结点8 (平衡因子2),
其实,这也是这行代码导致的:
// avlNode的右指针指向right的左子树
avlNode.right = right.left;
不难看出,只要有一种办法能让结点5和结点7不再拥有共同的父结点,并且父结点属于兄弟关系,就能让整棵树平衡,而这个办法就是RL旋转,顾名思义,就是先右旋转,再左旋转,那么,对于图2.2.3,先将结点8右旋转(