“不平衡”出现的时机
在上一篇 AVL树基础 文章中我们最后说到“平衡因子”概念。
在插入新元素后,就可能出现“不平衡”,所以我们就需要去维护平衡。首先我们图解分析“不平衡”出现的时机。
本文首发于心安-XinAnzzZ 的个人博客,转载请注明出处~
可以看到,在插入新的节点之后,导致了父节点的高度值变化,从而导致父节点或者祖先节点(父节点的父节点)的左右子树的高度差变化,也就出现了“不平衡”的状态。
所以,我们就应该每次插入新节点的时候通过平衡因子的变化来判断是否导致了父节点或者祖先节点“不平衡”,从而来维护平衡因子,使得这棵树继续平衡。
由于我们的插入的代码逻辑是递归完成的,所以我们维护了父节点的平衡,也就递归的维护了祖先节点的平衡。
在上一篇文章中,我们写了两个辅助函数,其中一个就是获取节点的平衡因子,这里我们就可以用上了。
在我们递归的插入元素的时候,递归的判断每一个节点的平衡因子,递归的维护平衡,从而使整棵树始终保持平衡。下面我们使用图形和代码来讲解一下如何通过左旋与右旋使得节点保持平衡。
图片演示左旋与右旋
右旋
如上图所示,插入一个新节点之后出现了“不平衡”,我们可以简单的将上图抽象为下图:
如上图,T1、T2、T3、T4分别为x、y、z节点的子树,根据二分搜索树的性质,它们之间的大小关系应该是
T1 < z < T2 < x < T3 < y < T4
右旋转就是将x节点的右子树先拿掉,然后让x节点的有孩子等于y节点,最后y节点的左孩子等于刚刚拿掉的x节点的右孩子。
这里无论T1、T2、T3、T4都是可以为空的,并不影响结果。右旋如下图:
可以看到,经过右旋之后,二叉树重新恢复平衡,并且上面的大小关系也没有发生改变,仍然满足二分搜索树的性质。
由于添加节点是递归过程,所以右旋之后的子树的父亲节点和祖先节点也会进行相应的右旋或者左旋使其自身达到平衡状态。
根据上图,我们可以编写我们的右旋转代码,如下:
/**
* LL
*
* / 对节点进行右旋转操作,返回右旋转之后新的根节点
* / y x
* / / \ / \
* / x T4 向右旋转(y) z y
* / / \ --------------> / \ / \
* / z T3 T1 T2 T3 T4
* / / \
* / T1 T2
*/
private Node rightRotate(Node y) {
Node x = y.left;
// 保存x节点的右子树,即使右子树为空,也没关系
Node t3 = x.right;
// 右旋
x.right = y;
// 将原本x的右子树放在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;
}
左旋
上图演示的只是其中一种情况,就是插入的新元素是在第一个不平衡的节点的左侧的左侧,所以可以使用右旋转来解决。
另外一种情况就是新插入的元素在第一个不平衡的节点的右侧的右侧,这里笔者偷个懒,不再画图。
因为这个其实和上面的是对称的,笔者可以自己在纸上画一下,对比下面的代码方便理解。
/**
* RR
*
* / 对节点进行左旋转操作,返回左旋转之后新的根节点
* / 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;
}
可以看到,这里的代码和上面的大同小异,也不需要过多的介绍,下面我们看一下最后两种情况。
LR
上面两种情况分别是插入的元素在不平衡节点的左侧的左侧(LL)、插入的元素在不平衡节点的右侧的右侧(RR)。
那么还有两种情况就分别是插入的元素在不平衡节点的左侧的右侧(LR)、插入的元素在不平衡节点的右侧的左侧(RL)
对于插入的元素在不平衡节点的左侧的右侧(LR),我们可以先进行左旋转,使得其转化为插入的元素在不平衡节点的左侧的左侧(LL),再进行右旋转即可。
图解:
对x节点进行左旋转之后就转化为了LL的情况,按照第一种情况进行右旋处理即可。
RL
同样的,和上面想对称的就是RL的情况,对称处理即可。这里笔者也不再赘述。
直接看一下实际的代码。上面我们已经写好了左旋与右旋的代码,四种情况其实用到的都是左旋和右旋,所以使用这两个函数即可。
插入一个元素的代码:
public void put(K key, V value) {
Node newNode = new Node(key, value);
root = put(root, newNode);
}
/*** 递归算法:插入一个元素,返回插入新节点后树的根 */
private Node put(Node node, Node newNode) {
if (node == null) {
size++;
return newNode;
}
if (newNode.key.compareTo(node.key) < 0) {
node.left = put(node.left, newNode);
} else if (newNode.key.compareTo(node.key) > 0) {
node.right = put(node.right, newNode);
} else {
node.value = newNode.value;
}
// 更新高度
node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;
// 维护平衡
int balanceFactor = getBalanceFactor(node);
// LL
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {
// 右旋转
return rightRotate(node);
}
// LR
if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
// 先对当前节点的左孩子进行左旋转转变为LL,然后在进行右旋转
node.left = leftRotate(node.left);
// 右旋转
return rightRotate(node);
}
// RR
if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {
// 左旋转
return leftRotate(node);
}
//RL
if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
// 先对当前节点的右孩子进行右旋转转变为RR,然后在进行左旋转
node.right = rightRotate(node.right);
// 左旋转
return leftRotate(node);
}
return node;
}
上面的代码注释写的都比较清楚了,读者只要认真思考一下,自己画一下各种树的结构,都是可以理解代码的思想的。
总结
笔者认为AVL树的维持平衡的思想理解起来相对来说还是有点难度的,所以这里做一下总结,希望能帮助到读者。
首先,AVL树基于二分搜索树,不同的是它要求每一个节点保持平衡。
那么在插入新元素的时候就可能会破坏原本的平衡性(当然删除元素也会,删除和添加是逆向操作,如果明白了本章内容,删除操作也是非常简单的,这一部分内容下一章再进行补充),所以就需要我们使用左旋或者右旋进行维护它的平衡。
那么不平衡的情况一共是四种,如下图:
针对不同的情况,做不同的旋转即可。再次强调,由于是递归算法,所以其父节点及祖先节点都会相应的做出改变来维护平衡。
本文到这里就结束了,后面笔者还会补充一下删除元素的代码。感谢阅读,有任何问题你都可以在评论区进行评论,笔者会在第一时间给你回复。
也欢迎加入笔者的qq群交流技术问题。
示例代码Github