目录
1、概念
1.1 什么是AVL树
2.1 平衡因子
3、AVL树节点的定义
4、AVL树的插入机制
4.1 初步插入节点
4.2 更新平衡因子
4.3 提升右树高度
4.3.1 右单旋
4.3.2 左右双旋
4.4 提升左树高度
4.4.1 左单旋
4.4.2 右左双旋
5、AVL树的验证
6、AVL树的删除
1、概念
1.1 什么是AVL树
AVL树是一颗二叉搜索树,且树中每个结点的左右子树高度之差的绝对值不超过 1。
二叉搜索树中,在数据乱序情况下其展现为一颗完全二叉树,具备优秀的数据查找能力,时间复杂度可以达到树的高度O(logN);但是在有序或接近有序的情况下,二叉搜索树为退化为一颗单分支树,其数据查找的时间复杂度也将退化为O(N),相当于顺序表,效率低下。
为解决二叉搜索树在极端情况下查找效率低下的问题,AVL树应运而生。
AVL树可以说是二叉搜索树的改良,在每次插入节点时保证每个节点的左右子树高度之差的绝对值不超过1,一旦超过1就要进行旋转调整操作,这样就能够降低树的高度,因此,提高了查找效率。
在AVL树中,其树高始终为O(logN),所以不管是在任何情况下,其查找效率始终稳定为O(logN)。
本篇博客就带大家一起探索AVL树其优雅搜索性能下的数据插入机制并完成代码实现。
2.1 平衡因子
平衡因子即为左右子树高度之差。
AVL树中每个节点的平衡因子都要满足绝对值 <= 1,在插入数据时一旦出现平衡因子 > 1 的情况就要立即对树做出旋转操作,使其重新调整为AVL树。
3、AVL树节点的定义
要实现AVL树,那么节点的定义是必不可少的,定义为主类AVLTree的静态内部类。
在AVL树中,使用孩子双亲表示法,并在每个节点中维护一个平衡因子。
static class TreeNode {
public int val;//节点值
public int bf;//平衡因子
public TreeNode left;//左孩子
public TreeNode right;//右孩子
public TreeNode parent;//父节点
//构造方法
public TreeNode(int val) {
this.val = val;
}
}
4、AVL树的插入机制
AVL树也是一颗二叉搜索树,所以新节点的插入操作与二叉搜索树一致。
AVL树的难点在于:插入新节点后如何判断是否仍满足AVL树、不满足AVL树后的旋转调整操作、调整完成后平衡因子的更新。
这些都是AVL树插入机制中的难点,接下来我们一步一步攻克。
4.1 初步插入节点
AVL树也是一颗二叉搜索树,所以新节点的插入操作与二叉搜索树一致,这里不再赘述,在下文给出代码。
4.2 更新平衡因子
新节点插入后,判断是否仍为AVL树,那就需要判断平衡因子bf是否 <= 1,也就是说需要对平衡因子进行更新再判断。
在本篇代码实现中,平衡因子定义为:右子树高度 - 左子树高度。
在插入节点后,我们需要更新平衡因子,在更新后,检查是否满足平衡条件,不满足则做出调整,具体过程如下:
- cur指针从新插入的节点node开始,向上更新平衡因子(整体为一个循环)
- 判断cur是其父节点parent的左孩子还是右孩子:若cur为其的左孩子则parent.bf-- ;若为其右孩子则parent.bf++。
- 若parent.bf = 0,说明新插入的节点使当前parent子树平衡,也不会影响上层树的平衡因子,而上层树本来就是平衡的,故当前整棵树仍为AVL树,插入成功,break即可。
- 若parent.bf = 1,只能说明当前parent子树是平衡的,而新插入的结点可能会导致到上层树不平衡,所以需要继续向上检查(cur = parent,parent = parent.parent),进行平衡因子的更新。
- 当平衡因子出现 >1 的情况时(parent.bf > 1),我们就需要对树进行旋转操作使其重新调整为AVL树(下文细讲,重点操作!!!),调整完成后整棵树旧平衡,break即可。
- parent.parent == null时,说明整棵树仍平衡,结束循环。
public boolean insert(int val) {
TreeNode node = new TreeNode(val);
if (root == null) {
//首次插入时
root = node;
return true;
}
TreeNode parent = null;
TreeNode cur = root;
while (cur != null) {
if (cur.val > val) {
parent = cur;
cur = cur.left;
}else if (cur.val < val) {
parent = cur;
cur = cur.right;
}else {
//节点不能重复
return false;
}
}
//cur == null
if (parent.val > val) {
parent.left = node;
}else {
parent.right = node;
}
node.parent = parent;
cur = node;
//更新平衡因子
while (parent != null) {
if (parent.left == cur) {
parent.bf--;
}else {
parent.bf++;
}
if (parent.bf == 0) {
break;
}
if (parent.bf == 2 || parent.bf == -2) {
if (parent.bf == -2) {
//左树高 --》 降低左树高度
if (cur.bf == -1) {
//右旋
rotateR(parent);
}else {
//左右双旋 cur.bf == 1
rotateLR(parent);
}
}else {
//右树高 --》 降低右树高度
if (cur.bf == 1) {
//左旋
rotateL(parent);
}else {
//右左双旋 cur.bf = -1
rotateRL(parent);
}
}
//调整完成
break;
}
cur = parent;
parent = parent.parent;
}
return true;
}
4.3 提升右树高度
插入节点后更新平衡因子时,若发现了parent.bf == -2的节点,则说明此时的树已不满足AVL树,且右子树高度-左子树高度=-2,说明parent节点的左子树高,右子树低,需要进行相关旋转操作,使树重新归于平衡状态。
4.3.1 右单旋
右单旋:即将部分节点旋转调整到右子树中,进而提升右子树高度,降低左子树高度。
当parent.bf == -2时,且cur.bf == -1时,这种情况下我们可以通过一次右单旋操作就可以使树重新归于平衡状态。
我们将右单旋操作定义为一个单独的方法抽象出来,形参为平衡因子 >1 的节点(即parent节点),subL为parent的左孩子,subLR为subL的右孩子。
- 我们定义parent的左孩子为subL,subL的右孩子为sunLR,通过画图我们发现,在旋转完成后parent节点成为了subL的右孩子,sunLR成为了parent的左孩子,我们可以改变连接关系完成旋转操作。
- 在旋转完成之后,需要更新平衡因子,细心观察可以发现,仅仅只有两个节点的平衡因子发生了改变,parent和subL节点,且均改变为0,进一步说明旋转完成后树重新归于平衡。
- 旋转过程中需要注意一些细节,比如:sunLR可能不存在,sunLR存在时(不为null)才可修改其parent域为parent;旋转前parent可能为根节点;旋转前parent可能具有父节点,这样也需要修改subL的parent域。
/**
* 右单旋
* @param parent:bf == 2的节点
*/
private void rotateR(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
//进行连接关系的修改,完成旋转操作
parent.left = subLR;
subL.right = parent;
if (subLR != null) {
//只有subLR存在时才可访问
subLR.parent = parent;
}
TreeNode pParent = parent.parent;
parent.parent = subL;
//旋转前parent可能具有父节点,需要修改subR的父亲指向
subL.parent = pParent;
if (pParent != null) {
if (pParent.left == parent) {
pParent.left = subL;
}else {
pParent.right = subL;
}
}else {
//parent为根节点时
root = subL;
}
//修改平衡因子
subL.bf = 0;
parent.bf = 0;
}
4.3.2 左右双旋
当parent.bf == -2时,且cur.bf == 1时,此时左树高度高于右树,但这时仅仅通过一次右单旋操作是不能使树重新平衡的。
此时需要进行左右双旋操作:先对cur树左单旋,再对parent树右单旋。
我们将左右双旋操作抽象成一个方法单独拿出来,形参为平衡因子 >1 的节点(即parent节点),定义subL为parent的左孩子,subLR为subL的右孩子。
此时,我们已经写出了单旋的代码,在这里直接调用即可,但是麻烦的问题又来了,经过两次的单旋操作后,我们发现节点平衡因子已经与实际不匹配了,这里该怎办呢?
其实,旋转后平衡因子的值其实与subLR旋转前的平衡因子数值有关,这里需要分情况讨论:
- 若旋转前subLR.bf == 1,则双旋后subLR.bf = 0,parent.bf = 0,subL.bf = -1。
- 若旋转前subLR.bf == -1,则双旋后subLR.bf = 0,parent.bf = 1,subL.bf = 0。
- 若旋转前subLR.bf == 0,则双旋后subLR.bf = 0,parent.bf = 0,subL.bf = 0。
情况一:若旋转前subLR.bf == 1,则双旋后subLR.bf = 0,parent.bf = 0,subL.bf = -1。
情况二:若旋转前subLR.bf == -1,则双旋后subLR.bf = 0,parent.bf = 1,subL.bf = 0。
情况三:若旋转前subLR.bf == 0,则双旋后subLR.bf = 0,parent.bf = 0,subL.bf = 0。
/**
* 左右双旋
* @param parent:bf == 2的节点
*/
private void rotateLR(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
int bf = subLR.bf;
rotateL(subL);
rotateR(parent);
if (bf == -1) {
subL.bf = 0;
subLR.bf = 0;
parent.bf = 1;
} else if (bf == 1) {
subL.bf = -1;
subLR.bf = 0;
parent.bf = 0;
}
//注意这里没有修改bf == 0情况时的平衡因子,
//是因为在上面进行双旋时平衡因子已经被修改好了
}
4.4 提升左树高度
若插入节点的父节点的平衡因子为0,说明整棵树仍旧平衡,插入成功直接break即可。
插入节点后更新平衡因子时,若发现了parent.bf == 2的节点,则说明此时的树已不满足AVL树,且右子树高度-左子树高度=2,说明parent节点的右子树高,左子树低,需要进行相关旋转操作,使树重新归于平衡状态。
4.4.1 左单旋
左单旋:即将部分节点旋转调整到左子树中,进而提升左子树高度,降低右子树高度。
当parent.bf == 2时,且cur.bf == 1时,这种情况下我们可以通过一次左单旋操作就可以使树重新归于平衡状态。
同样将左单旋操作定义为一个单独的方法抽象出来,形参为平衡因子 >1 的节点(即parent节点),subR为parent的右孩子,subRL为subR的左孩子。
- 我们定义parent的右孩子为subR,subR的左孩子为sunRL,通过画图我们发现,在旋转完成后parent节点成为了subR的左孩子,sunRL成为了parent的右孩子,我们可以改变连接关系完成旋转操作。
- 在旋转完成之后,需要更新平衡因子,细心观察可以发现,仅仅只有两个节点的平衡因子发生了改变,parent和subR节点,且均改变为0,进一步说明旋转完成后树重新归于平衡。
- 旋转过程中需要注意一些细节,比如:sunRL不为null时才可修改其parent域为parent;旋转前parent可能为根节点;旋转前parent可能具有父节点,这样也需要修改subR的parent域。
/**
* 左单旋
* @param parent:bf == 2的节点
*/
private void rotateL(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
//进行连接关系的修改,完成旋转操作
subR.left = parent;
parent.right = subRL;
TreeNode pParent = parent.parent;
parent.parent = subR;
if (subRL != null) {
//只有subRL不为空时才可访问
subRL.parent = parent;
}
//旋转前parent可能具有父节点,需要修改subR的父亲指向
subR.parent = pParent;
if (pParent != null) {
if (parent == pParent.left) {
pParent.left = subR;
}else {
pParent.right = subR;
}
}else {
//parent为根节点时
root = subR;
}
//修改平衡因子
parent.bf = 0;
subR.bf = 0;
}
4.4.2 右左双旋
当parent.bf == 2时,且cur.bf == -1时,此时右树高度高于左树,但这时仅仅通过一次左单旋操作同样也是不能使树重新平衡的。
我们需要进行右左双旋操作:先对cur树右单旋,再对parent树左单旋。
我们将右左双旋操作定义为一个单独的方法抽象出来,形参为平衡因子 >1 的节点(即parent节点),subR为parent的右孩子,subRL为subR的左孩子。
旋转完成后,平衡因子的数值与subRL旋转前的平衡因子数值有关,我们需要分类讨论。
- 若旋转前subRL.bf == 1,则双旋后parent.bf = -1,subRL.bf = 0,subR.bf = 0。
- 若旋转前subRL.bf == -1,则双旋后parent.bf = 1,subRL.bf = 0,subR.bf = 0。
- 若旋转前subRL.bf == 0,则双旋后parent.bf = 0,subRL.bf = 0,subR.bf = 0。
情况一:若旋转前subRL.bf == 1,则双旋后parent.bf = -1,subRL.bf = 0,subR.bf = 0。
情况二:若旋转前subRL.bf == -1,则双旋后parent.bf = 1,subRL.bf = 0,subR.bf = 0。
情况三:若旋转前subRL.bf == 0,则双旋后parent.bf = 0,subRL.bf = 0,subR.bf = 0。
/**
* 右左双旋
* @param parent:bf == 2的节点
*/
private void rotateRL(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
int bf = subRL.bf;
rotateR(subR);
rotateL(parent);
if (bf == -1) {
parent.bf = 0;
subRL.bf = 0;
subR.bf = 1;
}else if (bf == 1) {
parent.bf = -1;
subRL.bf = 0;
subR.bf = 0;
}
//注意这里没有修改bf == 0情况时的平衡因子,
//是因为在上面进行双旋时平衡因子已经被修改好了
}
5、AVL树的验证
在实现插入机制后,如何验证一棵树是否是AVL树呢?
我们可以通过以下几个条件判断我们所构建出的是否为AVL树:
- 满足二叉搜索树(中序遍历得到升序序列)
- 每个节点子树高度差的绝对值 <= 1
- 每个节点的平衡因子的数值正确
/**
* 中序遍历 --》 判断是否为二叉搜索树
* @param root
*/
public void inorder(TreeNode root) {
if (root == null) {
return;
}
inorder(root.left);
System.out.println(root.val);
inorder(root.right);
}
/**
* 求树的高度
* @param root
* @return
*/
public int height(TreeNode root) {
if (root == null) {
return 0;
}
int leftH = height(root.left);
int rightH = height(root.right);
return Math.max(leftH,rightH)+1;
}
public boolean isBalance(TreeNode root) {
if (root == null) {
return true;
}
int leftH = height(root.left);
int rightH = height(root.right);
//平衡因子就是 错 的情况下
if (root.bf != rightH-leftH) {
return false;
}
//根节点和左右子树都要平衡
return leftH-rightH <= 1 && isBalance(root.left) && isBalance(root.right);
}
6、AVL树的删除
面试时,关于AVL树插入操作的思想和代码问到的比较多,而删除问的就比较少了,最多也就问个思想。这里留下删除操作的思想:
- 找到需要删除的节点
- 按照搜索树的删除规则删除节点
- 更新平衡因子,如果出现了不平衡,进行旋转(单旋,双旋)。
END