本文已参与「新人创作礼」活动,一起开启掘金创作之路。
说起面试题目,几乎大部分同学都有一个疑问:为什么各个公司的面试官总是很喜欢问AVL树和红黑树的问题?其实我们的日常开发中经常会跟红黑树打交道,而AVL树又跟红黑树关系匪浅。下面我们就先了解一下AVL树。
二叉查找树
磨刀不误砍柴工,我们先回顾一下二叉查找树。
二叉查找树(Binary Search Tree,简称BST树,也叫二叉搜索树、二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树:
(1)若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)它的左、右子树也分别为二叉查找树。
上面两棵都是二叉查找树,可以看到,对一棵二叉查找树进行中序遍历即可得到一个递增序列。
查询在二叉查找树T中查找x的过程为:
- 若T是空树,则搜索失败;
- 若x等于T的根结点的值,则查找成功;
- 若x小于T的根结点的值,则递归查找左子树;
- 若x大于T的根结点的值,则递归查找右子树。
插入在二叉查找树T中插入x的过程为:
- 若T是空树,则将x作为根结点插入;
- 若x小于T的根结点的值,则插入到左子树中;
- 若x大于T的根结点的值,则插入到右子树中。
删除在二叉查找树T中删除x的过程为:
- 若x是叶子结点,删除x;
- 若x只有一个左孩子或者一个右孩子,将x与子结点互换,删除x;
- 若x既有左孩子又有右孩子,则将x在中序遍历中的前驱结点或者后继结点与x互换,转化为前两种情况,再进行处理。
前两种情况比较容易理解,这里重点解释下第三种情况。比如我们要删除下面这棵树的结点15,那怎样才能让这棵树尽量维持原貌?马上想到,对一棵二叉查找树进行中序遍历即可得到一个递增序列。那用跟结点15大小关系最接近的结点来代替它的位置不就可以了吗!
如果用前驱结点12跟15交换,那就演变成了情况(1);如果用后继结点18跟15交换,那就演变成了情况(2)。有没有可能前驱结点或者后继结点有两个子结点?不可能的。有两个子结点,就没办法成为待删除结点的前驱结点或者后继结点了。
最好的情况下,查找、插入、删除的效率都是O(logn);最差情况下,二叉查找树演变成一个链表,查找、插入、删除效率都是O(n)。
那怎样避免二叉查找树演变成链表呢?AVL树就能解决这个问题!
AVL树
AVL树是具有如下性质的二叉查找树:
(1)每个结点的左右子树高度之差的绝对值(平衡因子)最多为1。
通过计算每个结点的平衡因子,那么很容易看到,上面两棵二叉查找树,第一棵是AVL树,第二棵不是。AVL树通过对平衡因子的限制,避免了演变为链表的情况。
查找
AVL树的查找过程跟二叉查找树的一样。上图是一种最坏情况下的AVL树,刚好每个结点的左子树高度 - 右子树高度 = -1。变成了一棵倾斜的AVL树。AVL树最坏情况下的理论最大高度1.44log(N+2)-1.328(数学问题,这里就不论证了)。
插入
AVL树的插入过程跟二叉查找树的一样,但是AVL树的平衡性有可能被打破,需要进行一些调整来重新满足平衡性限制。
首先需要找到离插入结点最近且平衡因子绝对值大于1的结点,以这个结点为根结点的子树就是最小不平衡子树。然后对这棵最小不平衡子树进行调整。下图这棵树中,最小不平衡子树就是以8为根结点的子树。
调整策略可以归纳为以下四种(规则看不懂?配合图片慢慢思考,其实非常简单!):
(1)LL型。给结点的左孩子(L)的左子树(L)中插入结点导致的失衡。
LL型调整规则(单右旋转):右旋,将B结点提升为A的父结点,A结点及其右子树变为B的右子树,B的右子树变为A的左子树。
(2)RR型。给结点的右孩子(R)的右子树(R)中插入结点导致的失衡。
RR型调整规则(单左旋转):左旋,将B结点提升为A的父结点,A结点及其左子树变为B的左子树,B的左子树变为A的右子树。
(3)LR型。给结点的左孩子(L)的右子树(R)中插入结点导致的失衡。
LR型调整规则(先左旋,再右旋):先左旋,将C结点提升为B的父结点,C的左子树变为B的右子树;再右旋,将C结点提升为子树的根结点,A结点及其右子树变为C的右子树,C的右子树变为A的左子树。
(4)RL型。给结点的右孩子(R)的左子树(L)中插入结点导致的失衡。
RL型调整规则(先左旋,再右旋):先左旋,将C结点提升为B的父结点,C的右子树变为B的左子树;再右旋,将C结点提升为子树的根结点,A结点及其左子树变为C的左子树,C的左子树变为A的右子树。
删除
AVL树的删除过程跟二叉查找树的一样,但是AVL树的平衡性有可能被打破,需要进行一些调整来重新满足平衡性限制。调整方式与插入过程的调整方式相同。
性能分析
AVL树查询、插入、删除的时间复杂度均为O(logn)。
最差情况下查询的效率约为1.44*O(logn)。
插入时间主要包含几个部分:寻找插入的位置O(logn)、插入操作O(1)、最小不平衡子树调整O(1)、回溯计算插入结点到根结点路径上各个结点平衡因子O(logn)。
删除时间主要包含几个部分:寻找待删除结点的位置+寻找待交换结点位置=O(logn)、将待删除结点交换到叶子结点上O(1)、删除交换后的叶子结点O(1)、最小不平衡子树调整O(1)、回溯计算删除结点到根结点路径上各个结点平衡因子+回溯过程中出现的最小不平衡子树调整O(logn)。