AVL 树是带有平衡条件的二叉查找树,所谓的平衡条件就是每个节点的左子树和右子树的高度最大差别为1。平衡二叉树的实现大部分过程和二叉查找树是一样的,区别在于要时刻保持树的平衡,所以在插入和删除之后要添加一个旋转算法来保持平衡,保持平衡需要借助一个节点高度的属性。
一、AVL 节点
1 | class AvlTree; |
AVL 节点同二叉查找树节点类似,这里可以额外添加一个节点的高度属性,来判断是否平衡。考虑到后面都需要函数来获取节点的高度以及尽量减少类对象的大小,这里单独通过函数获取高度属性并比较(后面程序)。
二、AVL 树class
1 | class AvlTree |
三、计算节点的高度
节点的高度:该节点到该树叶子节点的最长路径的长。
定义:空二叉树的高度为-1,只有根节点的二叉树的高度为0。
//计算节点的高度
1 | int AvlTree::Height(AvlNode *node) |
递归:不必纠结于内部实现,明确其功能即可。节点存在儿子,高度累加。
四、节点的插入
递归:不必纠结于内部实现,明确其功能即可。节点存在儿子,高度累加。
四、节点的插入
这是平衡二叉树的重头戏,分几部分讨论。只有当节点的插入打乱了平衡才需要调整,也就是节点的插入导致某个节点 X 的两棵子树的高度差2,因为插入之前树一定是平衡的(子树高度差最大为1),这种不平衡出现在一下四种情况:
插入点位于 X 的左子节点的左子树——左左;
插入点位于 X 的左子节点的右子树——左右;
插入点位于 X 的右子结点的左子树——右左;
插入点位于 X 的右子结点的右子树——右右;
具体的就不说了,随便参考哪本靠谱点的数据结构书,上面都有详细介绍
由于只有 “插入点至根节点” 路径上的各节点可能改变平衡状态,因此,只要调整其中最深(区分深度和高度)的那个节点,便可以使整棵树重新获得平衡,调整最深的节点就是调整不平衡节点当中距离根节点最长的那个节点。
另外,所有的旋转调整后的最终结果必须遵循二叉查找树的规则。
如上图(来源于《STL源码剖析》)右图中违反AVL tree规则的最深的节点是 18,所以调整之后,节点 22 还是根节点,且其右子树不变,后面我们会验证。
4.1 插入点位于 X 的左子节点的左子树——左左
上面违反AVL-tree平衡条件的是节点18,其不平衡是由于在其左子节点14的左子树插入新结点11所致,插入节点13也是左左,A 所划定的区域都属于左子节点的左子树范围。
左左类型用单旋转就可以调整,代码如下
1 | //左左单旋转 |
指针的引用当做指针的指针来理解
4.2 插入点位于 X 的右子节点的右子树——右右
//右右单旋转
1 | void AvlTree::RotationRightOnce(AvlNode *&k2) |
注意的是调整后需要修改不平衡区域的根节点。
4.3 插入点位于 X 的左子节点的右子树——左右
注意的是调整后需要修改不平衡区域的根节点。
4.3 插入点位于 X 的左子节点的右子树——左右
这种情况和右左少许有些特殊,不能通过单旋转达到平衡状态,先看左右情况,就是插入点15位于是不平衡节点18左子节点14的右子树(B和C都是右子树部分)
上面单次旋转不能调整平衡,需要进行两次旋转,也就是双旋转。关键就在于两次旋转的先后顺序,从上图可知,要保持平衡只能k2,也就是节点16作为新的根节点,由于16节点比较深,需要进行两次旋转,每次旋转之后16节点深度降低一层,也就是单旋转之后要保证节点16成为该旋转区域的根节点。需要清楚的是单次旋转只能将某节点的深度降低一层。首先需要对不平衡节点18的左子树进行旋转,要想将16旋转之后成为其左子树的根节点,只能对其左子树进行右旋转,旋转之后节点16就成为了节点18的左子节点,然后以节点18作为根据点进行左旋转,这样节点16就成为了根节点,所有旋转都是满足二叉查找树规则的。
1 | //左右双旋转 |
由上面可知,单旋转就是在满足二叉查找树的基础上,将某节点的深度降低一层,获得平衡。左旋转就是将左子节点降低一层,右旋转就是将右子结点降低一层(深度降低)。
4.4 插入点位于 X 的右子节点的左子树——右左
这种情况和4.3情况类似,上面已经详细介绍过了,这里就不画图分析了。
//右左双旋转
1 | void AvlTree::RotaionRightTwice(AvlNode *&k3) |
根据4.3的分析,从上面代码就可以直接看出,两次旋转过程,第一次单旋转就是将k3节点的右子结点的左子节点旋转为该区域根节点,也就是成为k3的右子结点,然后以k3为根据点将其右子结点旋转为新的根节点。两次旋转之后,新的根节点就是原根节点k3的右子结点的左子节点。其余节点的变换则基于二叉查找树规则。
有了上面的分析,节点的插入就好办了。与二叉查找树的区别在于需要添加一个旋转的过程。
根据4.3的分析,从上面代码就可以直接看出,两次旋转过程,第一次单旋转就是将k3节点的右子结点的左子节点旋转为该区域根节点,也就是成为k3的右子结点,然后以k3为根据点将其右子结点旋转为新的根节点。两次旋转之后,新的根节点就是原根节点k3的右子结点的左子节点。其余节点的变换则基于二叉查找树规则。
有了上面的分析,节点的插入就好办了。与二叉查找树的区别在于需要添加一个旋转的过程。
1 | //插入节点 |