一、简介
本文将通过图解和代码详细讲解AVL平衡二叉树的性质及失衡和再平衡的内容。在看本文之前希望大家具备二分搜索树的相关知识。或移步《二分搜索树》了解二分搜索树。
二、平衡二叉树
前面关于二分搜索树的文章,最后分析了在极端情况下,二分搜索树会退化为一个链表,那为了避免这种情况的发生,AVL平衡二叉树应运而生。
平衡二叉树的定义:
1 平衡二叉树是一颗二分搜索树,及平衡二叉树满足二分搜索树的所有性质 2 平衡二叉树要求任意一个节点的左右子树的高度差不能超过1
对于第一点应该挺容易理解的,对于第二点,我们要做一点解释。对于高度差,有一个专有名词平衡因子。
平衡因子:左子树的高度减去右子树的高度,及B = B左 - B右。由平衡二叉树的定义可知,平衡因子的取值只可能为0,1,-1。0:左右子树等高。1:左子树比较高。-1:右子树比较高。如下图
高度:一般的我们取叶子节点的高度值为1,任意一个节点的高度值取左右子树比较高的那个孩子节点的高度值然后加1。比如上图1中20这个节点的高度值,显然左子树比较高,所以H20 = H10 + 1;依次类推,H10 = H6(或者H14) + 1 = 2;所以H20 = 3;
上图1的树的各个节点的高度,如下图1所示。各个节点的平衡因子如下图2红色数字所示。所以根据定义,各个节点的左右子树的高度差不能超过1,及任意一个节点的平衡因子应该为 -1, 0, 1;所以下面的这棵树是一棵平衡二叉树。
三、AVL的失衡及再平衡--旋转
3.1、AVL的失衡情况
前面我们介绍了AVL的高度和平衡因子问题,接下来我们来看看有几种情况会导致AVL的失衡,也就是什么情况下我们需要调整AVL树,使其经过调整后能继续维持平衡状态。
如上图1所示,当我们已经插入了20,10元素,当我们再插入6这个元素的时候,很显然20节点的平衡因子为2,及B20 = 2 > 1此时该树已经不平衡了。
如上图2所示,当我们已经插入了20,10元素,当我们再插入14这个元素的时候,很显然20节点的平衡因子为2,及B20 = 2 > 1此时该树已经不平衡了。
如上图3所示,当我们已经插入了20,29元素,当我们再插入33这个元素的时候,很显然20节点的平衡因子为-2,及B20 = -2 < -1此时该树已经不平衡了。
如上图4所示,当我们已经插入了20,29元素,当我们再插入25这个元素的时候,很显然20节点的平衡因子为-2,及B20 = -2 < -1此时该树已经不平衡了。
对于AVL,需要进行再平衡操作的情况正如以上4个图所示。那接下来我们需要讨论的问题就是如何调整了,及旋转。
3.2、AVL的再平衡--旋转
如下图所示,对于上一章我们说的四种失衡状态的调整。然后我们加下来对照着下面的四张图片进行逐一介绍旋转的过程。
3.2.1、LL旋转
LL,及对于上图1的待旋转的树(中间那棵),造成这棵树失衡的节点6在20节点的左孩子的左孩子处,left,left简称为LL。这个时候我们需要进行的旋转一般称之为LL旋转。对于这种情况,我们考虑如何旋转,始终要考虑如何通过旋转既达到再平衡的目的,又能维持平衡二叉树的性质不变,即左孩子 < 父节点 < 右孩子。观察图一中插入节点6以后,一个很显然的结果就是不管我们怎么旋转只有当10节点在中间的时候我们才能保证这棵树是平衡的。我们知道了结果,再看看这个旋转的过程,为了保证平衡二叉树的性质,根据左孩子 < 父节点 < 右孩子的性质,我们看20 > 10,也就是说,我们可以将20节点下移,放到10节点的右孩子处,即得到图1中的结果。
为了更好地描述这个过程,我们使用几个虚拟的节点。
/////////////////////////////////////////////////// // LL T1// // y x // // / \ / \ // // x T4 向右旋转 (y) z y // // / \ - - - - - - - -> / \ / \ // // z T3 T1 T2 T3 T4 // // / \ // // T1 T2 // ///////////////////////////////////////////////////
如上所示,我们真实的三个节点为Y > X > Z。然后我们为了方便描述,增加几个虚拟的节点,节点间的大小关系:T1 对于LL,我们要右旋才能达到再平衡,根据之前描述,我们需要将Y节点顶替T3的位置,问题来了,T3放哪呢?根据大小关系 X < T3 < Y。我们可以将T3放到Y的左孩子节点的位置,这样进行旋转后得到的结果如上所示。我们发现这棵树不但达到了再平衡的目的,节点间的大小关系,依然维持了:T1 代码实现一下这个过程,先假设我们的节点为Node。传入的参数应该是Y节点 对于以上代码,结合上面我们分析的过程,大家应该很容易就能理解。 RR对应上面的图3,RR及造成AVL失衡的节点6在20节点的右侧的右侧,即RR。对于RR我们要进行左旋转才能实现再平衡。同样的,我们如果想通过旋转达到再平衡,AVL树的性质依然是我们实现这个操作的根本。如上图3所示,如果我们将20节点移到29元素的左孩子节点处,便可实现再平衡。而且也能维持AVL树的基本性质。 同分析LL一样,我们增加一些虚拟节点来描述这个过程。 节点间的大小关系:T1 代码实现就不解释了 LR对应上图2,即造成AVL失衡的节点14在节点20的左侧的右侧,即LR。这种情况有点复杂,而且有个很想当然的坑,就是将根节点直接换成10不就完事了?可是如果我们这么做,发现,10的左节点为14,不满足:左孩子 < 父节点 < 右孩子的大小关系了。这种情况呢,正确的做法是先将10节点左旋,然后再将14节点右旋。大家通过之前对LL和RR的分析,在脑子中能不能想象到这个画面呢? 为了方便描述,我们依然增加一些虚假的节点来描述这个过程。 对于原始的这棵树呢,大小关系:T1 我们发现经过我们的分析,将这种复杂的情况进行一步步的拆解即分解成了比较简单的情况。不得不感叹一下:计算机的世界太神奇了。 呃呵,自己看吧,不解释,不接受反驳。皮一下,很开心。 相信大家看到这里,被面试官虐千百遍的问题,原来不过如此。其实一切高大上的问题,只要我们耐心的看下去就能有收获。 首先需要明白,AVL的失衡是由于节点的变动引起的,也就是增和删操作才会导致节点的变动。下面我们结合平衡因子和插入或者删除的过程,分析AVL再平衡的时机。 增加操作再平衡时机: 对于3.2章节中的过程,希望大家可以清楚,我们是通过眼睛观察来判断AVL是不是失衡了,但是计算机还没有达到这种能力。所以我们想想前面介绍的平衡因子,正是判断AVL是不是平衡的重要依据。假如现在向一棵空的AVL树依次插入[20,10,6];三个节点。当插入6节点后,如下图所示,各个节点的高度。之前说过:B = H左 - H右。我们看一下20这个节点的平衡因子,B20 = 2 - 0 = 2 > 1;所以,这时20就是不平衡的节点,需要对20这个节点进行旋转才能再平衡。但是从元素插入操作看一下,很显然当插入6这个元素的时候,并不知道20这个节点的平衡因子已经不满足要求了。需要沿着添加的元素向上回溯,沿着该节点到根节点的路径,一步步的重新计算其父节点,爷爷节点,祖父节点...当我们发现其父节点,爷爷节点...等平衡因子不满足要求的时候,就对该节点进行旋转。 删除操作再平衡的时机: 删除操作进行再平衡的时机类似增加操作,需要在删除节点后沿着其父节点,爷爷节点...一直向上计算各个节点的平衡因子是否满足AVL的性质。当发现某个节点的平衡因子不在[-1, 1]之间的时候,然后判断其形状对应的进行左旋转或者右旋转使其完成再平衡。 在前面的章节详细介绍了AVL的定义,失衡,再平衡即LL,RR,LR,RL等旋转。接下来我们通过代码实现一棵AVL树。对于我们要实现的AVL平衡二叉树,我们期待具备的功能如下: 在实现增删改查之前我们先设计两个辅助方法,如下所示,getHeight方法获取节点的高度值,getBalanceFactor方法获取节点的平衡因子。 在《二分搜索树》介绍了二分搜索树,前面根据AVL的定义可知,AVL是完全满足一个二分搜索树的所有性质的,如果大家想搞明白AVL,还是建议去先去看一下二分搜索树。对于二分搜索树的添加操作的代码实现,如下所示: 如果你还没法理解上面的代码,请移步《二分搜索树》。对于AVL的添加操作,无非就是在AVL中需要考虑在二分搜索树失衡的时候,如何通过旋转达到再平衡。根据我们前面的介绍,我们应该已经很明白旋转的思路了,那我们直接上代码吧。 看上面代码,辅之旋转示意图和平衡因子,相信大家能通过自己的分析,根据当前节点和左右孩子的平衡因子能判断出来是LL,LR,RR,或者RL的情况。 至于左右旋转,我们前文给出了代码,但当我们对节点进行旋转以后,我们需要重新维护一下各个节点的高度值。所以经过完善的左右旋转的代码如下: 对于删除,稍微复杂一点,但是基本思路和增加是一样的。但是关于我觉得还是有必要给大家恶补一下二分搜索树的删除的思路,当我们删除一个节点的时候,待删除节点左右子树有一个为空,我们只需要将其不为空的子树的根节点提到待删除元素的位置即可。如果其左右子树都不为空,则将其右子树最小的元素提到待删除节点处。详细的讨论请参阅《二分搜索树》,在二分搜索树删除操作的基础上,我们只需要辅之再平衡操作即可。 前面我们实现了一棵AVL树,我们如何验证这到底是不是一棵AVL树呢?这个问题,我们依然是从其定义来思考,首先,AVL是一棵二分搜索树,其次每个节点的平衡因子能满足在[-1, 1]之间,能满足这两点其实加之我们代码的逻辑即可判断其是不是一棵AVL树了。 二分搜索树有一个延伸出来的性质不知道大家还记不记得,对于二分搜索树的中序遍历,其实是对二分搜索树从小到大排序的过程。那我们判断中序遍历的结果满不满足从小到大即可判定其是不是一棵二分搜索树。 我们写如下测试代码: 到此,AVL所有的内容我们已经介绍完了。哎呦,凌晨三点了,心疼自己一秒钟。我爱我的国。 参考文献: 《玩转数据结构-从入门到进阶-刘宇波》 《数据结构与算法分析-Java语言描述》 如有错误的地方还请留言指正。 原创不易,转载请注明原文地址:https://www.cnblogs.com/hello-shf/p/11352071.html 1 private Node rightRotate(Node y) {
2 Node x = y.left;
3 Node T3 = x.right;
4
5 // 向右旋转过程
6 x.right = y;
7 y.left = T3;
8
9 return x;
10 }
3.2.2、RR旋转
////////////////////////////////////////////////
// RR T1
1 private Node leftRotate(Node y) {
2 Node x = y.right;
3 Node T2 = x.left;
4
5 // 向左旋转过程
6 x.left = y;
7 y.right = T2;
8
9 return x;
10 }
3.2.3、LR
//////////////////////////////////////////////////////////////////////////////////////////
// LR T1
3.2.4、RL
//////////////////////////////////////////////////////////////////////////////////////////
// RL: T1
3.3、再平衡的时机
四、代码实现一棵AVL
1 以Node作为链表的基础存储结构
2 使用泛型,并要求该泛型必须实现Comparable接口
3 基本操作:增删改查
4.1、AVL的基础代码
1 /**
2 * 描述:AVL 平衡二叉树的实现
3 *
4 * @Author shf
5 * @Date 2019/7/31 15:35
6 * @Version V1.0
7 **/
8 public class AVL
1 /**
2 * 获得节点node的高度
3 * @param node
4 * @return
5 */
6 private int getHeight(Node node){
7 if(node == null)
8 return 0;
9 return node.height;
10 }
11
12 /**
13 * 获得节点node的平衡因子
14 * @param node
15 * @return
16 */
17 private int getBalanceFactor(Node node){
18 if(node == null)
19 return 0;
20 return getHeight(node.left) - getHeight(node.right);
21 }
4.2、增
1 /**
2 * 添加元素
3 * @param e
4 */
5 public void add(E e){
6 root = add(root, e);
7 }
8
9 /**
10 * 添加元素 - 递归实现
11 * 时间复杂度 O(log n)
12 * @param node
13 * @param e
14 * @return 返回根节点
15 */
16 public Node add(Node node, E e){
17 if(node == null){// 如果当前节点为空,则将要添加的节点放到当前节点处
18 size ++;
19 return new Node(e);
20 }
21 if(e.compareTo(node.e) < 0){// 如果小于当前节点,递归左孩子
22 node.left = add(node.left, e);
23 } else if(e.compareTo(node.e) > 0){// 如果大于当前节点,递归右孩子
24 node.right = add(node.right, e);
25 }
26 return node;
27 }
1 /**
2 * 向以node为根的二分搜索树中插入元素(key, value),递归算法
3 * 时间复杂度 O(log n)
4 * @param node
5 * @param key
6 * @param value
7 * @return 返回插入新节点后二分搜索树的根
8 */
9 private Node add(Node node, K key, V value){
10
11 if(node == null){
12 size ++;
13 return new Node(key, value);
14 }
15
16 if(key.compareTo(node.key) < 0)
17 node.left = add(node.left, key, value);
18 else if(key.compareTo(node.key) > 0)
19 node.right = add(node.right, key, value);
20 else // key.compareTo(node.key) == 0
21 node.value = value;
22
23 // 更新height
24 node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
25
26 // 计算平衡因子
27 int balanceFactor = getBalanceFactor(node);
28
29 // 平衡维护
30 //////////////////////////////////////////////////////
31 // LL T1
4.3、左旋和右旋
1 /**
2 * 对节点y进行向右旋转操作,返回旋转后新的根节点x
3 * @param y
4 * @return
5 */
6 ///////////////////////////////////////////////////
7 // LL T1
4.4、删
1 /**
2 * 从二分搜索树中删除键为key的节点
3 * @param key
4 * @return
5 */
6 public V remove(K key){
7
8 Node node = getNode(root, key);
9 if(node != null){
10 root = remove(root, key);
11 return node.value;
12 }
13 return null;
14 }
15
16 /**
17 * 删除指定的节点
18 * @param node
19 * @param key
20 * @return
21 */
22 private Node remove(Node node, K key){
23
24 if( node == null )
25 return null;
26
27 Node retNode;
28 if( key.compareTo(node.key) < 0 ){
29 node.left = remove(node.left , key);
30 // return node;
31 retNode = node;
32 }
33 else if(key.compareTo(node.key) > 0 ){
34 node.right = remove(node.right, key);
35 // return node;
36 retNode = node;
37 }
38 else{ // key.compareTo(node.key) == 0 找到待删除的节点 node
39
40 // 待删除节点左子树为空,直接将右孩子替代当前节点
41 if(node.left == null){
42 Node rightNode = node.right;
43 node.right = null;
44 size --;
45 // return rightNode;
46 retNode = rightNode;
47 }
48
49 // 待删除节点右子树为空,直接将左孩子替代当前节点
50 else if(node.right == null){
51 Node leftNode = node.left;
52 node.left = null;
53 size --;
54 // return leftNode;
55 retNode = leftNode;
56 }
57
58 // 待删除节点左右子树均不为空的情况
59 else{
60 // 待删除节点左右子树均不为空
61 // 找到右子树最小的元素,替代待删除节点
62 Node successor = minimum(node.right);
63 //successor.right = removeMin(node.right);
64 successor.right = remove(node.right, successor.key);
65 successor.left = node.left;
66
67 node.left = node.right = null;
68
69 // return successor;
70 retNode = successor;
71 }
72 }
73
74 if(retNode == null)
75 return null;
76
77 // 更新height
78 retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));
79
80 // 计算平衡因子
81 int balanceFactor = getBalanceFactor(retNode);
82
83 // 平衡维护
84 // LL
85 if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0)
86 return rightRotate(retNode);
87
88 // RR
89 if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0)
90 return leftRotate(retNode);
91
92 // LR
93 if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0) {
94 retNode.left = leftRotate(retNode.left);
95 return rightRotate(retNode);
96 }
97
98 // RL
99 if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0) {
100 retNode.right = rightRotate(retNode.right);
101 return leftRotate(retNode);
102 }
103
104 return retNode;
105 }
106 /**
107 * 返回以node为根的二分搜索树的最小值所在的节点
108 * @param node
109 * @return
110 */
111 private Node minimum(Node node){
112 if(node.left == null)
113 return node;
114 return minimum(node.left);
115 }
4.5、其他操作
1 /**
2 * 返回以node为根节点的二分搜索树中,key所在的节点
3 * @param node
4 * @param key
5 * @return
6 */
7 private Node getNode(Node node, K key){
8
9 if(node == null)
10 return null;
11
12 if(key.equals(node.key))
13 return node;
14 else if(key.compareTo(node.key) < 0)
15 return getNode(node.left, key);
16 else // if(key.compareTo(node.key) > 0)
17 return getNode(node.right, key);
18 }
19
20 /**
21 * 判断是否包含 key
22 * @param key
23 * @return
24 */
25 public boolean contains(K key){
26 return getNode(root, key) != null;
27 }
28
29 /**
30 * 获取指定 key 的 value
31 * @param key
32 * @return
33 */
34 public V get(K key){
35
36 Node node = getNode(root, key);
37 return node == null ? null : node.value;
38 }
39
40 /**
41 * 设置 key 对应元素的值 value
42 * @param key
43 * @param newValue
44 */
45 public void set(K key, V newValue){
46 Node node = getNode(root, key);
47 if(node == null)
48 throw new IllegalArgumentException(key + " doesn't exist!");
49
50 node.value = newValue;
51 }
五、验证AVL
1 /**
2 * 测试方法 - 判断该二叉树是否是一棵二分搜索树
3 * @return
4 */
5 public boolean isBST(){
6
7 ArrayList
public class Main {
public static void main(String[] args) {
AVL