手撕AVL_二叉平衡树(图文并茂)
目录
前言
大家好,今天带大加手撕AVL树的插入
一 . AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年 发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。 一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在搜索时间复杂度。
二 . AVL树节点的定义
为了AVL树实现简单,AVL树节点在定义时维护一个平衡因子,具体节点定义如下:
static class TreeNode{ int val; int bf; // 平衡因子 -> 当前节点的平衡因子=右子树高度-左子树的高度 TreeNode left;// 节点的左孩子 TreeNode right;// 节点的右孩子 TreeNode parent;// 节点的双亲 public TreeNode(int val){ this.val = val; } }
注意: 当前节点的平衡因子=右子树高度-左子树的高度。但是,不是每棵树,都必须有平衡因子,这只是其中的一种实现 方式。
三 . AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可 以分为两步:
1.二分查找树的形式进行插入 while(){ 1.1 当前节点的val小于待插入节点的val -> 往右边迭代 1.2 当前节点的val大于待插入节点的val -> 往左边迭代 1.3 当前节点的val等于待插入节点的val -> 不允许插入 } 1.4 正式插入节点 2.调节平衡因子 2.1 如果当前节点是父节点的左节点 parent.bf++ 2.2 如果当前节点是父节点的右节点 parent.bf--; 2.3 根据parent.bf判断是否需要继续向上调整 2.3.1 如果parent.bf == 0 表示当前树平衡 2.3.2 如果parent.bf == (1 || -1) 当前子树平衡,子树上面的情况未知,主要向上进行调整 2.3.3 如果parent.bf == (2 || -2) 需要进行旋转,根据不同的情况,采取不同的旋转策略
1.插入节点
本来还想偷个懒的,发现二叉搜索树没有写,这就没办法了,一步一步来吧
假如我们需要在下面avl树中插入节点10
parent: 表示当前遍历到的节点的父节点 初始值为null
child: 表示当前遍历到的节点 初始值为root 即 5号节点
1.4 正式插入节点
给出部分代码
TreeNode parent = null;
TreeNode child = root;
// 1.二分查找树的插入
while(child != null){
if(child.val > val){ //
// 往左边插入
parent = child;
child = child.left;
}else if(child.val < val){
// 往右边插入
parent = child;
child= child.right;
}else{
// 树中已经存在该节点,不允许插入
return false;
}
}
// 节点正式插入
if(parent.val > val){
parent.left = node;
}else{
parent.right = node;
}
node.parent = parent;
child = node;
至此第一步完美结束.我们开始第二大步调节平衡因子
2.调节负载因子
负载因子的调节和二叉树的旋转可以说是平衡树的核心了,但是只要理解透了,就是随便写,毫无难度!
- 2.1 如果当前节点是父节点的左节点 parent.bf++
- 2.2 如果当前节点是父节点的右节点 parent.bf--;
此时如果我们插入的元素使10,那parent.bf == 1
2.3.2 如果parent.bf == (1 || -1) 当前子树平衡,子树上面的情况未知,需要向上进行调整
以9为根节点的子树确实是平衡了,但是上面的子树我们并不清楚,可能平衡也可能不平衡,上面的两个都是不平衡的,我们来看一个平衡的例子
上面的意思是当前子树平衡,子树上面的情况未知,需要向上进行调整我们只知道以parent为根节点的子树的情况,并不清楚上面的情况
再次回到上面插入10的例子中,根据2.3.2可知需要向上调整
2.3.3 如果parent.bf == (2 || -2) 需要进行旋转,根据不同的情况,采取不同的旋转策略
到这就是开始旋转了
旋转一共分为四种,表示四种不同的情况,我先把这四种旋转应用的情况列举出来,具体如何旋转等到下面会细说
1.左单旋
2.右单旋
3.左右双旋
4.右左双旋
这四种旋转就是AVL树最核心的地方了,相比大家也看出来了,在神魔情况下用哪一种旋转,我都用蓝圈圈起来了,这里来总结一下
1.如果 parent.bf 和 child.bf 都是正数,那么应该进行左单旋(同正左旋)
2.如果 parent.bf 和 child.bf 都是负数,那么应该进行右单旋(同异右旋)
3.如果 parent.bf 为正 child.bf为负 ,右左双旋
4.如果parent.bf为负 child.bf 为正 ,左右双旋
记的话估计不太好记,但是有一点一定可以记住,同单异双(同号单旋,异号双旋),大家还是重在理解
给出部分代码
// 2.调节平衡因子
while(parent != null){
// 先看child是parent的左还是右,判断bf是++还是--
if(child == parent.right){
// child是parent的左树 bf++
parent.bf++;
}else{
// child是parent的右树 bf++
parent.bf--;
}
// 根据parent的负载因子判断是否需要继续向上调整 bf[-1,0,1]
if(parent.bf == 0){
// 当前子树平衡代表上面的也已经平衡
break;
}else if(parent.bf == 1 || parent.bf == -1){
// 上面的树不一定平衡,继续向上调整
child = parent;
parent = parent.parent;
}else {
if (parent.bf == 2) {
if (child.bf == 1) {
// 左单旋
rotateLeft(parent);
} else if (child.bf == -1) {
// 右左双旋
rotateRL(parent);
}
} else {
if (child.bf == -1) {
// 右单旋
rotateRight(parent);
} else if (child.bf == 1) {
// 左右双旋
rotateLR(parent);
}
}
// 上述代码走完平衡!!
break;
}
}
下面就是真正写旋转的代码了,我会带着大家实现一个单旋和一个双旋,剩下的就是照着葫芦画瓢,相比大家应该没什么问题
四 . AVL树的旋转
1.左单旋
我们先定义变量,不需要为了节省空间搞成parent.parent.left.right.left 举个例子,实际上没有这么多嵌套
现在来想一个问题: 如果60往上提,那以40为根节点的子树该放在哪? 想清楚这个问题,那么恭喜你
你完全有能力去手撕后面的代码,即使一些细节你考虑不到! 不要往下看,自己想想吧,下面的图会有助于大家理解的
首先要明白一个问题,60提上去之后成为了这颗子树的根节点,根据二叉搜索树的性质,40应该放在60的左侧,答案依然明了,30的左子树指向60的左子树
到这里已经可以手撕一部分代码了
以为到这里就忘了吗? nonono 在这里发出灵魂三问
1. 30是否还有父节点? 如果有的话不改变指向的话会怎么样?
2.subRL有没有可能为空? 如果为空会不会发生空指针异常?
3.parent有没有可能为root?
都是一些细节问题,直接给出代码,大家自行理解,理解不了评论区留言
private void rotateLeft(TreeNode parent) {
// 定义变量
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
// 保存parent的父节点 问题1
TreeNode pParent = parent.parent;
// 问题2 增加空指针判断
if(subRL != null) subRL.parent = parent;
parent.right = subRL;
subR.left = parent;
parent.parent = subR;
// 问题3 增加判断
if(parent == root){
root = subR;
root.parent = null;
}else{
if(pParent.left == parent){
pParent.left = subR;
}else{
pParent.right = subR;
}
subR.parent = pParent;
}
// 根据图示修改平衡因子
parent.bf = subR.bf = 0;
}
右旋
public void rotateRight(TreeNode parent){
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
TreeNode pParent = parent.parent;
if(subLR != null) subLR.parent = parent;
parent.parent = subL;
parent.left = subLR;
subL.right = parent;
if(parent == root){
// 当前的parent是根节点
root = subL;
root.parent = null;
}else{
if(pParent.left == parent){
pParent.left = subL;
}else{
pParent.right = subL;
}
subL.parent = pParent;
}
parent.bf = subL.bf = 0;
}
2.左右双旋
左右双旋无非就是经过两次旋转,只要单旋搞清楚了,双旋完全没有难度,值的一提的是,平衡因子的修改需要根据subLR.bf来进行判断,除此之外,没有其他的细节了
/*
* 右左双旋
* */
private void rotateRL(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
int bf = subRL.bf;
rotateRight(subR);
rotateLeft(parent);
if(bf == -1){
parent.bf = subRL.bf = 0;
subR.bf = 1;
}else{
subRL.bf= subR.bf = 0;
parent .bf = -1;
}
}
/*
* 左右双旋
* */
private void rotateLR(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
int bf = subLR.bf;
rotateLeft(subL);
rotateRight(parent);
if(bf == -1){
subL.bf = subLR.bf = 0;
parent.bf = -1;
}else{
subL.bf = -1;
subLR.bf = parent.bf = 0;
}
}
最后来验证一下是否成功,主要是根据中序遍历的结果是否有序和左右子树的高度差不超过一来进行验证
五 . AVL树性能分析
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询 时高效的时间复杂度,即 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要 维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要 一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修 改,就不太适合。
总结
大家多多理解,我们下一篇博客见