数据结构之AVL树
平衡树和AVL
我们先来回忆一下二分搜索树所存在的一个问题:当我们按顺序往二分搜索树添加元素时,那么二分搜索树可能就会退化成链表。例如,现在有这样一颗二分搜索树:
接下来我们依次插入如下五个节点:7、6、5、4、3。按照二分搜索树的特性,这棵树就会变成如下这样:
可见在极端的情况下,如果往一棵二分搜索树添加元素时,完全是按照顺序添加的,那么此时二分搜索树就会退化成链表,$O(logn)$ 时间复杂度退化到 $O(n)$。
这是因为二分搜索树不具有自平衡的特性,为了让二分搜索树不退化成链表,我们就得设计一种机制,即便是在按顺序添加元素时,也能让二分搜索树维持平衡。而具有自平衡特性的二叉树或 m 叉树,就称之为平衡树。
而这个“平衡”其实有几种情况,有绝对平衡:任意节点的左右子树高度相等(2-3树);高度平衡:任意节点的左右子树高度相差不超过 1(AVL树);近似平衡:任意节点的左右子树高度相差不超过 2,或者说从根节点到叶子节点的最长路径不大于最短路径的 2 倍(红黑树)。
基本上只要一棵树的高度和节点数量之间的关系始终是 $O(logn)$,也就是不会发生退化情况的,就能称之为平衡树。如果敢说一棵树是”平衡“的,就意味着它的高度是 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,右子树不存在,所以该节点的平衡因子是 $1-0=1$
- 节点7的左子树不存在,右子树高度是1,所以平衡因子是 $0-1=-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),单次右旋转
- 右右情况( 8000 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; }
- 数据结构之AVL树
- [算法] 数据结构之AVL树
- 数据结构 -- AVL树
- 数据结构之AVL树
- 数据结构:自平衡二叉查找树(AVL树)
- 数据结构之如何判断一棵二叉树是否是平衡二叉树(AVL树)
- 数据结构之---C语言实现平衡二叉树(AVL树)
- 看数据结构写代码(56) 平衡二叉树(AVL树)
- 数据结构之AVL树
- 【数据结构与算法基础】AVL树 / AVL Tree
- 数据结构之AVL树详解
- 数据结构之AVL树
- 数据结构——AVL树的插入
- [数据结构笔记]树-二叉搜索树-AVL树
- 数据结构之AVL树
- 数据结构之 AVL树(平衡二叉树)(C语言实现)
- 数据结构54:平衡二叉树(AVL树)
- 【数据结构】初入数据结构中的平衡二叉搜索树(AVL树)及Java实现
- 数据结构之AVL树
- 高性能流媒体服务器-nebula之数据结构(3)--AVL树定时器