找往期文章包括但不限于本期文章中不懂的知识点:
个人主页: 我要学编程(ಥ_ಥ)-CSDN博客
所属专栏:数据结构(Java版)****
二叉搜索树的学习 我们在这篇文章中学习了二叉搜索树,知道了当插入的元素序列趋于有序时,将会导致 其效率变得十分底下。
今天,我们就来学习AVL树。
目录
AVL树的相关概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年 发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过 1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树要么是空树,要么是具有以下性质的二叉搜索树: 它的左右子树都是AVL树 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)。如下图所示:
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 log n,搜索时间复杂度O(log n)。 因此AVL树也叫平衡的二叉搜索树。
平衡因子的概念:一棵AVL树的右子树高度 减去 左子树的高度之差的结果。
AVL树的相关操作
首先,得定义一个树节点:
java
class TreeNode {
public int val;
// 根结点的右子树高度-左子树的高度
public int balanceFactor; // 平衡因子
public TreeNode parent; // 父亲
public TreeNode left; // 左子树
public TreeNode right; // 右子树
public TreeNode(int val) {
this.val = val;
}
}
java
public TreeNode root;
注意:
1、这里采用的是孩子双亲表示法;
2、一个新插入的节点,其平衡因子默认是0(因为其左子树和右子树皆为null,高度之差为0)。
AVL树的插入
思路:首先,得和二叉搜索树一样,找到合适的位置插入节点。
代码实现:
java
public boolean insert(int val) {
// 如果root为null,就直接插入元素即可
if (root == null) {
root = new TreeNode(val);
return true;
}
// 开始寻找要插入的元素位置
TreeNode cur = root;
TreeNode parent = null;
while (cur != null) {
parent = cur;
if (cur.val < val) {
cur = cur.right;
} else if (cur.val > val) {
cur = cur.left;
} else {
return false; // 不能插入两个相同的元素
}
}
// 插入元素
TreeNode node = new TreeNode(val);
if (parent.val > val) {
parent.left = node;
} else {
parent.right = node;
}
node.parent = parent;
当插入成功之后,就得开始检查整棵树的平衡因子是否符合要求。但是整棵树的平衡因子都得检查一遍吗?不是的,我们在哪里插入的节点,就从该位置开始向上调整来修改平衡因子。如果平衡因子修改后符合我们的要求,则继续往上调整,直至parent为null即可,如果不符合要求,就得开始处理这种情况。处理完成之后肯定是符合我们的要求的,因此便可以不再进行检查了。
思路:从插入节点的位置开始向上调整:看插入节点的位置是处于其parent的哪边:如果是右边,就让平衡因子++,如果是左边就让平衡因子--。接着再进行判断:如果parent的平衡因子的绝对值大于1的话,就得进行调整;如果等于1的话,就继续往上判断;如果等于0的话,就说明已经平衡了,不需要再进行判断了。
代码实现:
java
// 判断是否是满足AVL树的条件:高度平衡(检查平衡因子)
cur = node;
while (parent != null) {
if (parent.left == cur) {
// 左树--
parent.balanceFactor--;
} else {
// 右树++
parent.balanceFactor++;
}
// 检查平衡因子的值
if (parent.balanceFactor == 0) {
// 说明已经平衡了
break;
} else if (parent.balanceFactor == 1 || parent.balanceFactor == -1) {
// 还得继续判断
cur = parent;
parent = cur.parent;
} else {
// 走到这里就说明平衡因子为2或者-2,是要进行修改的
if (parent.balanceFactor == 2) { // 右树高-->降低右树的高度
if (cur.balanceFactor == 1) {
// 左旋
rotateLeft(parent);
} else {
// 先右旋,再左旋(右左双旋)
ratateRL(parent);
}
} else { // 左树高-->降低左树的高度
if (cur.balanceFactor == 1) {
// 先左旋,再右旋(左右双旋)
ratateLR(parent);
} else {
// 右旋
rotateRight(parent);
}
}
// 调整完就说明已经是平衡的状态了,可以不用继续进行调整了
break;
}
}
return true;
注意:
1、平衡因子是右子树的高度减去左子树的高度之差。因此,插入的节点位置为parent的右子树时,parent的平衡因子就得++;反之,就得--。
2、旋转的条件判断:
右旋
1、右旋的条件:全部是左树高(parent的左树高,cur的左树高),因此往右旋,以降低左树的高度,来达到平衡。
这里之所以把 cur.right 放到 parent 的左边,是因为 cur.right 肯定小于parent,且其位置被parent占有,因此只能往这里放。
代码实现:
java
private void rotateRight(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
TreeNode parentP = parent.parent;
// 修改四个指针的指向
parent.parent = subL;
parent.left = subLR;
// 判断是否为null
if (subLR != null) {
subLR.parent = parent;
}
subL.right = parent;
// 修改高树的指向和本级新的根结点指向
if (parent == root) {
root = subL;
subL.parent = null;
} else {
subL.parent = parentP; // 先得保存下来
if (parentP.left == parent) {
parentP.left = subL;
} else {
parentP.right = subL;
}
}
// 修改平衡因子
subL.balanceFactor = 0;
parent.balanceFactor = 0;
}
左旋
2、左旋的条件:全部是右树高(parent的右树高,cur的右树高),因此往左旋,以降低右树的高度,来达到平衡。
代码实现:
java
private void rotateRight(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
TreeNode parentP = parent.parent;
// 修改四个指针的指向
parent.parent = subL;
parent.left = subLR;
// 判断是否为null
if (subLR != null) {
subLR.parent = parent;
}
subL.right = parent;
// 修改高树的指向和本级新的根结点指向
if (parent == root) {
root = subL;
subL.parent = null;
} else {
subL.parent = parentP; // 先得保存下来
if (parentP.left == parent) {
parentP.left = subL;
} else {
parentP.right = subL;
}
}
// 修改平衡因子
subL.balanceFactor = 0;
parent.balanceFactor = 0;
}
左右双旋
3、左右双旋的条件:先左旋,再右旋(parent的左树高,cur的右树高) :先左旋cur,再右旋parent,降低左树高度,再降低右树高度,达到平衡。
上面分别是 cur.right无子树、有左子树无右子树、有右子树无左子树的三种情况,情况不同对应的平衡因子处理的方式就不相同。 并且当cur.right 处于无子树的时候,经过左旋和右旋代码的处理,平衡因子已经处于一种正常的状态。
代码实现:
java
private void ratateLR(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
// 通过subLR的平衡因子来判断节点的位置,从而确定调整后的平衡因子
int bf = subLR.balanceFactor;
// 先左旋parent的孩子节点
rotateLeft(parent.left);
// 再右旋parent节点
rotateRight(parent);
// 调整平衡因子、
// 前面的旋转调整只满足一种情况:
// 即平衡因子全为0的情况
if(bf == -1) {
subL.balanceFactor = 0;
subLR.balanceFactor = 0;
parent.balanceFactor = 1;
}else if (bf == 1){
subL.balanceFactor = -1;
subLR.balanceFactor = 0;
parent.balanceFactor = 0;
}
}
右左双旋
4、右左双旋的条件: 先右旋,再左旋(parent的右树高,cur的左树高):先右旋cur,再左旋parent,降低右树的高度,再降低左树的高度,达到平衡。
上面分别是 cur.left无子树、有左子树无右子树、有右子树无左子树的三种情况,情况不同对应的平衡因子处理的方式就不相同。 并且当cur.left 处于无子树的时候,经过左旋和右旋代码的处理,平衡因子已经处于一种正常的状态。
代码实现:
java
private void ratateRL(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL= subR.left;
// 通过subRL的平衡因子来判断节点的位置,从而确定调整后的平衡因子
int bf = subRL.balanceFactor;
// 先右旋parent的孩子节点
rotateRight(parent.right);
// 再左旋parent节点
rotateLeft(parent);
// 调整平衡因子、
// 前面的旋转调整只满足一种情况:
// 即平衡因子全为0的情况
if(bf == 1) {
parent.balanceFactor = -1;
subR.balanceFactor = 0;
subRL.balanceFactor = 0;
}else if(bf == -1) {
parent.balanceFactor = 0;
subR.balanceFactor = 1;
subRL.balanceFactor = 0;
}
}
通过上面的代码,一棵AVL树就被构建出来了。但是还得去证明这棵树是正确的AVL树。因为AVL树是一棵高度平衡的二叉搜索树,所以我们只要能同时证明是二叉搜索树,并且是一棵二叉平衡树即可。
证明一棵二叉树是AVL树
1、证明是一棵二叉搜索树:
思路:只需要中序遍历这棵树即可。如果中序遍历的结果是有序的,那么就是一棵二叉搜索树。
代码实现:
java
public void inorder(TreeNode root) {
if (root == null) {
return;
}
inorder(root.left);
System.out.print(root.val+" ");
inorder(root.right);
}
2、证明是一棵二叉平衡树:
思路:只要证明每棵树都是二叉平衡树 && 计算出来的平衡因子和每个节点对应的平衡因子相同即可。 证明每棵树都是二叉平衡树,就是先证明根结点是,再去递归证明左子树和右子树。在这里免不了去求树的高度。
代码实现:
java
public boolean isBalanceTree(TreeNode root) {
if (root == null) {
return true;
}
// 判断根结点是不是一棵二叉平衡树 + 其左右子树是不是一棵二叉平衡树
// 二叉平衡树:左右子树的高度差 <= 1
int leftHigh = TreeHigh(root.left);
int rightHigh = TreeHigh(root.right);
// 检查平衡因子
if(rightHigh - leftHigh != root.balanceFactor) {
System.out.println(root.val+" 平衡因子异常!");
//return false; // 可有可无
}
if (Math.abs(rightHigh-leftHigh) > 1) {
return false;
}
// 再判断左右子树是不是二叉平衡树
return isBalanceTree(root.left) && isBalanceTree(root.right);
}
// 求树的高度:根节点(1)+Math.max(左子树,右子树)
private int TreeHigh(TreeNode root) {
if (root == null) {
return 0;
}
if (root.left == null && root.right == null) {
return 1;
}
return 1 + Math.max(TreeHigh(root.left), TreeHigh(root.right));
}
验证代码写出来之后,就可以开始去测试了。
思路:就是看是否同时满足上面两个条件即可。
代码实现:
java
public class Test {
public static void main(String[] args) {
AVLTree avlTree = new AVLTree();
int[] array = {16, 3, 7, 11, 9, 26, 18, 14, 15}; // 常见场景
//int[] array = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14}; // 特殊场景
// 插入元素
for (int i = 0; i < array.length; i++) {
avlTree.insert(array[i]);
}
// 中序遍历
avlTree.inorder(avlTree.root);
System.out.println();
// 判断是否为二叉平衡树
System.out.println(avlTree.isBalanceTree(avlTree.root));
}
}
AVL树的删除步骤:
1、找到要删除的节点
2、和删除二叉搜索树一样,分为四种情况(具体可见上面的链接)
3、删除成功之后,就得要更新平衡因子(左树降低,平衡因子就++;反之,则--)。
注意:删除之后的调整是向上调整,可能会调整到根节点。
AVL树的性能分析
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log N。
但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。因此就有了红黑树,我们下一期再学习。
好啦!本期 数据结构之AVL树 的学习之旅就到此结束啦!我们下一期再一起学习吧!