面试官提到的 AVL 树,到底是个啥

每天早上七点三十,准时推送干货

了解过平衡二叉树的朋友们,对它一定有印象,今天阿粉就与大家一起深入了解一下AVL树!

一、摘要

在上篇文章,我们详细的介绍了二叉树的算法以及代码实践,我们知道不同的二叉树形态结构,对查询效率也会有很大的影响,尤其是当树的形态结构变成一个链条结构的时候,查询最后一个元素的效率极底,如何解决这个问题呢?

关键在于如何最大限度的减小树的深度,从而提高查询效率,正是基于这一点,平衡二叉查找树出现了!

平衡二叉查找树,算法由Adel'son-Vel'skiiLandis两位大神发明,同时也俗称AVL 树,来自两位大神的姓名缩写,特性如下:

  • 它的左子树和右子树都是平衡二叉树;

  • 且它的左子树和右子树的深度之差的绝对值(平衡因子 ) 不超过1;

简单的说,就是为了保证平衡,当前节点的左子树、右子树的高度差不超过1!

废话也不多说了,直奔主题,算法思路如下!

二、算法思路

平衡二叉查找树的查找思路,与二叉树是一样,每次查询的时候对半分,只查询一部分,以达到提供效率的目的,插入、删除也一样,最大的不同点:每次插入或者删除之后,需要计算节点高度,然后按需进行调整!

如何调整呢?主要方法有:左旋转、右旋转!

下面我们分别来分析一下插入、删除的场景调整。

2.1、插入场景

我们来分析一下插入的场景,如下:

场景一

当我们在40的左边或者右边插入的时候,也就是50的左边,只需绕80进行右旋转,即可达到树高度差不超过1!

场景二

当我们在60的左边或者右边插入的时候,也就是50的右边,需要进行两次旋转,先会绕50左旋转一次,再绕80右旋转一次,即可达到树高度差不超过1!

场景三

当我们在120的左边或者右边插入的时候,也就是90的右边,只需绕80进行左旋转,即可达到树高度差不超过1!

场景四

当我们在85的左边或者右边插入的时候,也就是90的左边,需要进行两次旋转,先会绕90右旋转一次,再绕80左旋转一次,即可达到树高度差不超过1!

总结

对于插入这种操作,总共其实只有这四种类型的插入,即:单次左旋转、单次右旋转、左旋转-右旋转、右旋转-左旋转,总结如下:

  • 当插入节点位于需要旋转节点的左节点的左子树时,只需右旋转;

  • 当插入节点位于需要旋转节点的左节点的右子树时,需要左旋转-右旋转;

  • 当插入节点位于需要旋转节点的右节点的右子树时,只需左旋转;

  • 当插入节点位于需要旋转节点的右节点的左子树时,需要右旋转-左旋转;

2.2、删除场景

接下来,我们分析一下删除场景!

其实,删除场景跟二叉树的删除思路是一样的,不同的是需要调整,删除的节点所在树,需要层层判断节点的高度差是否大于1,如果大于1,就进行左旋转或者右旋转!

场景一

当删除的节点,只有左子树时,直接将左子树转移到上层即可!

场景二

当删除的节点,只有右子树时,直接将右子树转移到上层即可!

场景三

当删除的节点,有左、右子树时,因为当前节点的左子树的最末端的右子树或者当前节点的右子树的最末端的左子树,最接近当前节点,找到其中任意一个,然后将其内容替换并移除最末端节点,即可实现删除!

总结

第三种场景稍微复杂了一些,但基本都是这么一个套路,与二叉树不同的是,删除之后需要判断树高,对超过1的进行调整,类似上面插入的左旋转、右旋转操作!

三、代码实践

接下来,我们从代码层面来定义一下树的实体结构,如下:

 1public class AVLNode<E extends Comparable<E>> {
 2
 3    /**节点关键字*/
 4    E key;
 5
 6    /**当前节点树高*/
 7    int height;
 8
 9    /**当前节点的左子节点*/
10    AVLNode<E> lChild = null;
11
12    /**当前节点的右子节点*/
13    AVLNode<E> rChild = null;
14
15    public AVLNode(E key) {
16        this.key = key;
17    }
18
19    @Override
20    public String toString() {
21        return "AVLNode{" +
22                "key=" + key +
23                ", height=" + height +
24                ", lChild=" + lChild +
25                ", rChild=" + rChild +
26                '}';
27    }
28}

我们创建一个算法类AVLSolution,完整实现如下:

  1public class AVLSolution<E extends Comparable<E>> {
  2
  3    /**定义根节点*/
  4    public AVLNode<E> root = null;
  5
  6    /**
  7     * 插入
  8     * @param key
  9     */
 10    public void insert(E key){
 11        System.out.println("插入[" + key + "]:");
 12        root = insertAVL(key,root);
 13    }
 14
 15    private AVLNode insertAVL(E key, AVLNode<E> node){
 16        if(node == null){
 17            return new AVLNode<E>(key);
 18        }
 19        //左子树搜索
 20        if(key.compareTo(node.key) < 0){
 21            //当前节点左子树不为空,继续递归向下搜索
 22            node.lChild = insertAVL(key,node.lChild);
 23            //出现不平衡,只会是左子树比右子树高,大于1的时候,就进行调整
 24            if(getHeight(node.lChild) - getHeight(node.rChild) == 2){
 25                if(key.compareTo(node.lChild.key) < 0){
 26                    //如果插入的节点位于当前节点的左节点的左子树,进行单次右旋转
 27                    node = rotateRight(node);
 28                }else{
 29                    //如果插入的节点位于当前节点的左节点的右子树,先左旋转再右旋转
 30                    node = rotateLeftRight(node);
 31                }
 32            }
 33        }else if(key.compareTo(node.key) > 0){
 34            //当前节点右子树不为空,继续递归向下搜索
 35            node.rChild = insertAVL(key,node.rChild);
 36            //出现不平衡,只会是右子树比左子树高,大于1的时候,就进行调整
 37            if(getHeight(node.rChild) - getHeight(node.lChild) == 2){
 38                if(key.compareTo(node.rChild.key) < 0){
 39                    //如果插入的节点位于当前节点的右节点的左子树,先右旋转再左旋转
 40                    node = rotateRightLeft(node);
 41                }else{
 42                    //如果插入的节点位于当前节点的右节点的右子树,进行单次左旋转
 43                    node = rotateLeft(node);
 44                }
 45            }
 46        } else{
 47            //key已经存在,直接返回
 48        }
 49        //因为节点插入,树高发生变化,更新节点高度
 50        node.height = updateHeight(node);
 51        return node;
 52    }
 53
 54    /**
 55     * 删除
 56     * @param key
 57     */
 58    public void delete(E key){
 59        root = deleteAVL(key,root);
 60    }
 61
 62    private AVLNode deleteAVL(E key, AVLNode<E> node){
 63        if(node == null){
 64            return null;
 65        }
 66        if(key.compareTo(node.key) < 0){
 67            //左子树查找
 68            node.lChild = deleteAVL(key,node.lChild);
 69            //可能会出现,右子树比左子树高2
 70            if (getHeight(node.rChild) - getHeight(node.lChild) == 2){
 71                node = rotateLeft(node);
 72            }
 73        } else if(key.compareTo(node.key) > 0){
 74            //右子树查找
 75            node.rChild = deleteAVL(key, node.rChild);
 76            //可能会出现,左子树比右子树高2
 77            if (getHeight(node.lChild) - getHeight(node.rChild) == 2){
 78                node = rotateRight(node);
 79            }
 80        }else{
 81            //找到目标元素,删除分三种情况
 82            //1.当前节点没有左子树,直接返回当前节点右子树
 83            //2.当前节点没有右子树,直接返回当前节点右子树
 84            //3.当前节点有左子树、右子树的时候,寻找当前节点的右子树的最末端的左子树,进行替换和移除
 85            if(node.lChild == null){
 86                return node.rChild;
 87            }
 88            if(node.rChild == null){
 89                return node.lChild;
 90            }
 91            //找到当前节点的右子树的最末端的左子树,也就是右子树最小节点
 92            AVLNode<E> minLChild = searchDeleteMin(node.rChild);
 93            //删除最小节点,如果高度变化,进行调整
 94            minLChild.rChild = deleteMin(node.rChild);
 95            minLChild.lChild = node.lChild;//将当前节点的左子树转移到最小节点上
 96
 97            node = minLChild;//覆盖当前节点
 98            //因为是右子树发生高度变低,因此可能需要调整
 99            if(getHeight(node.lChild) - getHeight(node.rChild) == 2){
100                node = rotateRight(node);
101            }
102        }
103        node.height = updateHeight(node);
104        return node;
105    }
106
107    /**
108     * 搜索
109     * @param key
110     * @return
111     */
112    public AVLNode<E> search(E key){
113        return searchAVL(key, root);
114    }
115
116    private AVLNode<E> searchAVL(E key, AVLNode<E> node){
117        if(node == null){
118            return null;
119        }
120        //左子树搜索
121        if(key.compareTo(node.key) < 0){
122            return searchAVL(key, node.lChild);
123        }else if(key.compareTo(node.key) > 0){
124            return searchAVL(key, node.rChild);
125        } else{
126            //key已经存在,直接返回
127            return node;
128        }
129    } 
130
131    /**
132     * 查找需要删除的元素
133     * @param node
134     * @return
135     */
136    private AVLNode<E> searchDeleteMin(AVLNode<E> node){
137        if (node == null){
138            return null;
139        }
140        while (node.lChild != null){
141            node = node.lChild;
142        }
143        return node;
144    }
145
146    /**
147     * 删除元素
148     * @param node
149     * @return
150     */
151    private AVLNode<E> deleteMin(AVLNode<E> node){
152        if(node == null){
153            return null;
154        }
155        if (node.lChild == null){
156            return node.rChild;
157        }
158        //移除最小节点
159        node.lChild = deleteMin(node.lChild);
160        //此时移除的是左节点,判断是否平衡高度被破坏
161        if(getHeight(node.rChild) - getHeight(node.lChild) == 2){
162            //进行调整
163            node = rotateLeft(node);
164        }
165        return node;
166
167    }
168
169    /**
170     * 单次左旋转
171     * @param node
172     * @return
173     */
174    private AVLNode<E> rotateLeft(AVLNode<E> node){
175        System.out.println("节点:" + node.key + ",单次左旋转");
176        AVLNode<E> x = node.rChild;//获取旋转节点的右节点
177        node.rChild = x.lChild;//将旋转节点的右节点的左节点转移,作为旋转节点的右子树
178        x.lChild = node;//将旋转节点作为旋转节点的右子树的左子树
179
180        //更新调整节点高度(先调整旋转节点node)
181        node.height = updateHeight(node);
182        x.height = updateHeight(x);
183        return x;
184    }
185
186    /**
187     * 单次右旋转
188     * @return
189     */
190    private AVLNode<E> rotateRight(AVLNode<E> node){
191        System.out.println("节点:" + node.key + ",单次右旋转");
192        AVLNode<E> x = node.lChild;//获取旋转节点的左节点
193        node.lChild = x.rChild;//将旋转节点的左节点的右节点转移,作为旋转节点的左子树
194        x.rChild = node;//将旋转节点作为旋转节点的左子树的右子树
195
196        //更新调整节点高度(先调整旋转节点node)
197        node.height = updateHeight(node);
198        x.height = updateHeight(x);
199        return x;
200    }
201
202    /**
203     * 左旋转-右旋转
204     * @param node
205     * @return
206     */
207    private AVLNode<E> rotateLeftRight(AVLNode<E> node){
208        System.out.println("节点:" + node.key + ",左旋转-右旋转");
209        //先对当前节点的左节点进行左旋转
210        node.lChild = rotateLeft(node.lChild);
211        //再对当前节点进行右旋转
212        return rotateRight(node);
213    }
214
215    /**
216     * 右旋转-左旋转
217     * @param node
218     * @return
219     */
220    private AVLNode<E> rotateRightLeft(AVLNode<E> node){
221        System.out.println("节点:" + node.key + ",右旋转-左旋转");
222        //先对当前节点的右节点进行右旋转
223        node.rChild = rotateRight(node.rChild);
224        return rotateLeft(node);
225
226    }
227
228    /**
229     * 获取节点高度,如果为空,等于-1
230     * @param node
231     * @return
232     */
233    private int getHeight(AVLNode<E> node){
234        return node != null ? node.height: -1;
235    }
236
237    /**
238     * 更新节点高度
239     * @param node
240     * @return
241     */
242    private int updateHeight(AVLNode<E> node){
243        //比较当前节点左子树、右子树高度,获取节点高度
244        return Math.max(getHeight(node.lChild), getHeight(node.rChild)) + 1;
245    }
246
247    /**
248     * 前序遍历
249     * @param node
250     */
251    public void frontTreeIterator(AVLNode<E> node){
252        if(node != null){
253            System.out.println("key:" + node.key);
254            frontTreeIterator(node.lChild);//遍历当前节点左子树
255            frontTreeIterator(node.rChild);//遍历当前节点右子树
256        }
257    }
258
259    /**
260     * 中序遍历
261     * @param node
262     */
263    public void middleTreeIterator(AVLNode<E> node){
264        if(node != null){
265            middleTreeIterator(node.lChild);//遍历当前节点左子树
266            System.out.println("key:" + node.key);
267            middleTreeIterator(node.rChild);//遍历当前节点右子树
268        }
269    }
270
271    /**
272     * 后序遍历
273     * @param node
274     */
275    public void backTreeIterator(AVLNode<E> node){
276        if(node != null){
277            backTreeIterator(node.lChild);//遍历当前节点左子树
278            backTreeIterator(node.rChild);//遍历当前节点右子树
279            System.out.println("key:" + node.key);
280        }
281    }
282
283}

测试代码,如下:

 1public class AVLClient {
 2
 3    public static void main(String[] args) {
 4        //创建一个Integer型的数据结构
 5        AVLSolution<Integer> avlTree = new AVLSolution<Integer>();
 6
 7        //插入节点
 8        System.out.println("========插入元素========");
 9        avlTree.insert(new Integer(100));
10        avlTree.insert(new Integer(85));
11        avlTree.insert(new Integer(120));
12        avlTree.insert(new Integer(60));
13        avlTree.insert(new Integer(90));
14        avlTree.insert(new Integer(80));
15        avlTree.insert(new Integer(130));
16        System.out.println("========中序遍历元素========");
17
18        //中序遍历
19        avlTree.middleTreeIterator(avlTree.root);
20        System.out.println("========查找key为100的元素========");
21
22        //查询节点
23        AVLNode<Integer> searchResult = avlTree.search(120);
24        System.out.println("查找结果:" + searchResult);
25        System.out.println("========删除key为90的元素========");
26
27        //删除节点
28        avlTree.delete(90);
29        System.out.println("========再次中序遍历元素========");
30
31        //中序遍历
32        avlTree.middleTreeIterator(avlTree.root);
33    }
34}

输出结果如下:

四、总结

平衡二叉树查找树,俗称AVL树,在查询的时候,操作与普通二叉查找树上的查找操作相同;插入的时候,每一次插入结点操作最多只需要单旋转或双旋转;如果是动态删除,删除之后必须检查从删除结点开始到根结点路径上的所有结点的平衡因子,也就是高度差,如果超过1就需要调整,最多可能需要O(logN)次旋转。

整体上来说,平衡二叉树优于普通二叉查找树!

五、参考

[1] 简书 - nicktming - 二叉平衡树: https://www.jianshu.com/p/22c00b3731f5

[2] iteye - Heart.X.Raid - 平衡二叉查找树 [AVL]: https://www.iteye.com/blog/hxraid-609949

< END >

如果大家喜欢我们的文章,欢迎大家转发,点击在看让更多的人看到。也欢迎大家热爱技术和学习的朋友加入的我们的知识星球当中,我们共同成长,进步。

往期精彩回顾

看懂这篇文章,玩转二叉查找树
如何创建一个完美的单例模式,你会么?

重点丨什么是双重检查锁模式?以及为何需要 volatile 关键字?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值