平衡树和AVL
我们先来回忆一下二分搜索树所存在的一个问题:当我们按顺序往二分搜索树添加元素时,那么二分搜索树可能就会退化成链表。例如,现在有这样一颗二分搜索树:
接下来我们依次插入如下五个节点:7、6、5、4、3。按照二分搜索树的特性,这棵树就会变成如下这样:
可见在极端的情况下,如果往一棵二分搜索树添加元素时,完全是按照顺序添加的,那么此时二分搜索树就会退化成链表, 时间复杂度退化到 。
这是因为二分搜索树不具有自平衡的特性,为了让二分搜索树不退化成链表,我们就得设计一种机制,即便是在按顺序添加元素时,也能让二分搜索树维持平衡。而具有自平衡特性的二叉树或 m 叉树,就称之为平衡树。
而这个“平衡”其实有几种情况,有绝对平衡:任意节点的左右子树高度相等(2-3树);高度平衡:任意节点的左右子树高度相差不超过 1(AVL树);近似平衡:任意节点的左右子树高度相差不超过 2,或者说从根节点到叶子节点的最长路径不大于最短路径的 2 倍(红黑树)。
基本上只要一棵树的高度和节点数量之间的关系始终是 ,也就是不会发生退化情况的,就能称之为平衡树。如果敢说一棵树是”平衡“的,就意味着它的高度是 logn 级别的。也就意味着对这棵树的基本操作(增删改查)是 logn 级别的。
其中 AVL 树是最早被发明出来的平衡树,AVL 这个名称来自于它的两位发明者 G.M. Adelson-Velsky 和 E.M. Landis 的首字母,AVL 树在他们1962年的论文中首次提出。所以,可以认为 AVL 树是最早的自平衡二分搜索树结构。AVL 树遵循的是高度平衡,任意节点的左右子树高度相差不超过 1。
计算节点的高度和平衡因子
经过以上的介绍,现在我们已经知道了AVL树是一种平衡的二分搜索树。那么为了维持AVL树的平衡,我们就得做一些额外的工作。首先,我们得知道AVL树的平衡状态,可以通过一些依据判断AVL树是否已经失衡了。如果处于失衡状态,就需要对AVL树做出一系列的调整使得它维持平衡。
判断AVL树是否平衡的主要依据是节点的平衡因子,而平衡因子则通过节点的高度计算得出。下图中,用黑色字体标记的是节点的高度,蓝色字体标记的是节点的平衡因子:
上图中的二叉树不是一棵合格的AVL树,因为只有当一棵二叉树所有节点的平衡因子都是 -1、0、1这 三个值时,这棵二叉树才能算是一棵合格的AVL树。如下图所示:
- 其中节点 4 的左子树高度是 1,右子树不存在,所以该节点的平衡因子是
- 节点7的左子树不存在,右子树高度是1,所以平衡因子是
- 所有的叶子节点,不存在左右子树,所以平衡因子都是 0
为了计算节点的平衡因子,我们需要在每个节点中新增加一个字段,存储节点的高度。而平衡因子的计算也很简单,用左子节点的高度减去右子节点的高度就可以了。也就是说,平衡因子就是左右子树高度的差值。
接下来,我们先实现AVL树的基础代码:
package tree.avl;
import java.util.ArrayList;
/**
* AVL树
*
* @author 01
* @date 2021-01-29
**/
public class AVLTree<K extends Comparable<K>, V> {
private class Node {
public K key;
public V value;
public Node left, right;
// 标识节点的高度
public int height;
public Node(K key, V value) {
this.key = key;
this.value = value;
left = null;
right = null;
// 新节点的默认高度
height = 1;
}
}
private Node root;
private int size;
public AVLTree() {
root = null;
size = 0;
}
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
/**
* 获得节点node的高度
*/
private int getHeight(Node node) {
return node == null ? 0 : node.height;
}
/**
* 获得节点node的平衡因子
*/
private int getBalanceFactor(Node node) {
if (node == null) {
return 0;
}
return getHeight(node.left) - getHeight(node.right);
}
}
检查二分搜索树性质和平衡性
有了判断平衡状态的依据后,我们就可以判断AVL树的平衡性了。除此之外,由于AVL树本质上是一棵平衡版的二分搜索树,所以我们还需要检查AVL树的二分搜索树性质。因为,调整AVL树的过程中可能会破坏二分搜索树的性质,此时就需要将其“矫正”过来。
判断AVL树的平衡性很简单,就是看各个节点的平衡因子是否大于1即可。因为平衡因子本质上只是左右子树高度的差值,而AVL树的定义是这个差值不能大于1。检查二分搜索树的性质也不难,通过中序遍历就可以做到。因为一棵树满足二分搜索树的性质,那么中序遍历必然是有序的,如果得到的结果是无序的就证明不满足二分搜索树的性质。
具体的实现代码如下:
/**
* 检查当前的AVL树是否满足二分搜索树的性质
*/
public boolean isBST() {
ArrayList<K> keys = new ArrayList<>();
inOrder(root, keys);
for (int i = 1; i < keys.size(); i++) {
// 中序遍历一棵二分搜索树所得到的key理应是有序的
// 如果是无序的,就证明不满足二分搜索树的性质
if (keys.get(i - 1).compareTo(keys.get(i)) > 0) {
return false;
}
}
return true;
}
/**
* 中序遍历以node为根的二叉树,并将每个节点的key放到keys中
*/
private void inOrder(Node node, ArrayList<K> keys) {
if (node == null) {
return;
}
inOrder(node.left, keys);
keys.add(node.key);
inOrder(node.right, keys);
}
/**
* 检查当前AVL树的平衡性
*/
public boolean isBalanced() {
return isBalanced(root);
}
/**
* 判断以Node为根的二叉树是否是一棵平衡二叉树,递归实现
*/
private boolean isBalanced(Node node) {
if (node == null) {
return true;
}
int balanceFactor = getBalanceFactor(node);
// AVL对平衡的定义是:左右子树高度相差不能大于1
if (Math.abs(balanceFactor) > 1) {
return false;
}
return isBalanced(node.left) && isBalanced(node.right);
}
旋转操作的基本原理
经过前面的铺垫,现在我们已经完成了AVL树维持平衡时所需的辅助功能。接下来,我们看看AVL树是怎么维持平衡的。首先,我们得知道AVL树什么时候会发生平衡性被打破的情况。
与其他树形结构一样,当AVL树添加或删除节点时,其平衡性就有可能会被打破。如下图所示:
那么AVL树是怎么维持平衡的呢?之前在红黑树的文章中提到过,红黑树是通过变色、左旋及右旋转这三种操作来维持平衡的。
因为AVL树中的节点没有颜色的概念,所以不存在变色的问题,只有左旋转、右旋转这两种维持平衡的操作。并且AVL树中的左旋转和右旋转,和之前红黑树中所介绍的是一样的。
左旋转:逆时针旋转红黑树的两个节点,使得父节点被自己的右子节点取代,而自己成为自己的左子节点。如下图:
- 在上图中,身为右子节点的Y取代了X的位置,而X变成了自己的左子节点,因此为左旋转
右旋转:顺时针旋转红黑树的两个节点,使得父节点被自己的左子节点取代,而自己成为自己的右子节点。如下图:
- 在上图中,身为左子节点的Y取代了X的位置,而X变成了自己的右子节点,因此为右旋转
那么AVL树什么时候需要进行左旋转,什么时候需要进行右旋转呢?这得看树的倾斜情况,因为不同的倾斜情况,需要采取不同的旋转方式。主要分为四种情况,对应着四种旋转方式。这里将其称为:
- 左左情况(LL),单次右旋转
- 右右情况(RR),单次左旋转
- 左右情况(LR),先左旋转,后右旋转
- 右左情况(RL),先右旋转,后左旋转
如果你有学习过如何还原魔方的话,就会发现AVL树的平衡过程跟魔方的还原非常相似。魔方的还原是有固定公式的:根据色块在一个面上的不同排列情况,都有相应的旋转步骤。只要跟着这个还原步骤,最终就能将魔方还原。
而AVL树的平衡大致过程就是:遇到什么样的节点排布,我们就对应怎么去旋转调整。只要按照这些固定的旋转规则来操作,就能将一个非平衡的AVL树调整成平衡的。这里不同的节点排布就对应着上述所说的四种情况,接下来我们就看看这四种情况及其解法。
1、左左情况(LL),简单来说就是整体左倾的情况,倾斜发生在节点左子树中的最左子节点。如下图:
- 图中的三角形表示各个节点的子树
在这种情况下,我们需要从下往上找到发生倾斜的子树的根节点,即该子树中平衡因子大于 1 的那个节点。在此例中就是 y 节点,此时我们以 y 节点为轴,进行一次右旋转,从而矫正这棵树:
2、右右情况(RR)是整体右倾的情况,倾斜发生在节点右子树中的最右子节点。如下图:
在这种情况下,同样从下往上找到相应的根节点,然后以根节点 y 为轴,进行一次左旋转:
3、左右情况(LR),倾斜发生在节点左子树中的最右子节点。如下图:
在这种情况下,我们就需要分两步走了,先以 x 节点为轴,进行左旋转:
可以看到此时就转换成了左左情况(LL),那么就只需要按照左左情况的方式,以 y 节点为轴,进行右旋转即可:
4、右左情况(RL),倾斜发生在节点右子树中的最左子节点。如下图:
同样,在这种情况下,我们也需要分两步走,先以 x 节点为轴,进行右旋转:
转换成了右右情况(RR)后,按照这种情况的方式,以 y 节点为轴,进行左旋转:
以上就是AVL树需要调整平衡的四种情况,以及四种对应的调整方式。现在让我们来看本小节最开始的那个例子,在该例子中,以节点4 为根的左子树出现了不平衡的情况。现在来看,该子树正好符合 “左左情况”。于是,我们以节点 4 为轴,进行右旋操作,就让AVL树重新恢复了高度平衡:
左旋转和右旋转的实现
在上一小节中,我们介绍了AVL树为了维持平衡所使用的旋转操作,以及不同情况所对应的不同旋转方式。在本小节中,就让我们用代码来实现AVL树的左旋转和右旋转操作。代码如下:
// 对节点y进行向右旋转操作,返回旋转后新的根节点x
// y x
// / \ / \
// x T4 向右旋转 (y) z y
// / \ - - - - - - - -> / \ / \
// z T3 T1 T2 T3 T4
// / \
// T1 T2
private Node rightRotate(Node y) {
Node x = y.left;
Node T3 = x.right;
// 向右旋转过程
x.right = y;
y.left = T3;
// 更新height
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
// 对节点y进行向左旋转操作,返回旋转后新的根节点x
// y x
// / \ / \
// T1 x 向左旋转 (y) y z
// / \ - - - - - - - -> / \ / \
// T2 z T1 T2 T3 T4
// / \
// T3 T4
private Node leftRotate(Node y) {
Node x = y.right;
Node T2 = x.left;
// 向左旋转过程
x.left = y;
y.right = T2;
// 更新height
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
向AVL树中添加元素
到目前为止,我们就已经了解了AVL树中维持平衡所需的内容。在理论和代码上我们都学习到了如何维持一棵AVL树的平衡性,也已经实现了相应的辅助功能。
那么也就知道在添加和删除元素时,如何解决可能破坏AVL树平衡性的问题。所以,接下来我们就实现向AVL树中添加元素的功能。具体代码如下:
/**
* 向AVL树中添加新的元素(key, value)
*/
public void add(K key, V value) {
root = add(root, key, value);
}
/**
* 向以node为根的AVL中插入元素(key, value),递归实现
* 返回插入新节点后AVL的根
*/
private Node add(Node node, K key, V value) {
if (node == null) {
size++;
return new Node(key, value);
}
if (key.compareTo(node.key) < 0) {
node.left = add(node.left, key, value);
} else if (key.compareTo(node.key) > 0) {
node.right = add(node.right, key, value);
} else {
node.value = value;
}
// 更新height
node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
// 计算平衡因子
int balanceFactor = getBalanceFactor(node);
// --- 维护平衡 start ---
// LL
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {
return rightRotate(node);
}
// RR
if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {
return leftRotate(node);
}
// LR
if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
// RL
if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
// --- 维护平衡 end ---
return node;
}
从AVL树中删除元素
从AVL树中删除元素也会打破AVL树的平衡性,那么在删除元素时如何维持AVL树的平衡呢?如果在删除元素时,打破了AVL树的平衡,其维持平衡的调整方式与之前提到的一样,还是根据那四种情况进行四种旋转操作即可。
因此,有了前面的基础,并且对二分搜索树的删除操作有一定的了解的话,那么对AVL树的删除操作理解起来就比较容易了。无非就是在二分搜索树的删除操作的基础上增加了维护平衡的操作,而这个操作与添加元素时是完全一样的。
我们来看个例子:
如上图所示,我们在AVL树中删除了节点 1,导致父节点 2 的平衡因子变为了 -2,打破了AVL树的平衡。此时,以节点 2 为根的子树正好形成了“右左情况(RL)”,于是我们首先以节点 4 为轴进行右旋转:
然后再以节点 2 为轴进行左旋转:
经过如上步骤后,最终AVL树重新恢复了高度平衡。
AVL树删除操作的具体实现代码如下:
/**
* 返回以node为根的AVL的最小值所在的节点
*/
private Node minimum(Node node) {
if (node.left == null) {
return node;
}
return minimum(node.left);
}
/**
* 从AVL中删除键为key的节点
*/
public V remove(K key) {
Node node = getNode(root, key);
if (node != null) {
root = remove(root, key);
return node.value;
}
return null;
}
/**
* 删除以node为根的AVL中键为key的节点,递归实现
* 返回删除节点后新的AVL的根
*/
private Node remove(Node node, K key) {
if (node == null) {
return null;
}
// 存放被删除的节点
Node retNode;
if (key.compareTo(node.key) < 0) {
// 待删除节点在左子树中
node.left = remove(node.left, key);
retNode = node;
} else if (key.compareTo(node.key) > 0) {
// 待删除节点在右子树中
node.right = remove(node.right, key);
retNode = node;
} else {
// 待删除节点左子树为空的情况
if (node.left == null) {
Node rightNode = node.right;
node.right = null;
size--;
retNode = rightNode;
}
// 待删除节点右子树为空的情况
else if (node.right == null) {
Node leftNode = node.left;
node.left = null;
size--;
// return leftNode;
retNode = leftNode;
}
// 待删除节点左右子树均不为空的情况
else {
// 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
// 用这个节点顶替待删除节点的位置
Node successor = minimum(node.right);
successor.right = remove(node.right, successor.key);
successor.left = node.left;
node.left = node.right = null;
retNode = successor;
}
}
if (retNode == null) {
return null;
}
// 更新height
retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));
// 计算平衡因子
int balanceFactor = getBalanceFactor(retNode);
// --- 维护平衡 start ---
// LL
if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0) {
return rightRotate(retNode);
}
// RR
if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0) {
return leftRotate(retNode);
}
// LR
if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0) {
retNode.left = leftRotate(retNode.left);
return rightRotate(retNode);
}
// RL
if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0) {
retNode.right = rightRotate(retNode.right);
return leftRotate(retNode);
}
// --- 维护平衡 end ---
return retNode;
}