文章目录:
1.4:平衡因子(balance factor 简称:bf)
1. AVL树(二叉平衡树)
1.1:背景
二叉搜索树虽然可以提高搜索的效率,但是如果序列有序or接近有序,二叉搜索树会退化成单支树。这种情况下搜索元素就相当于在顺序表中查找,效率反而低下。
两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis对此提出了解决方案:向二叉搜索树中插入新的节点后,如果可以保证每个节点的左右子树高度差不超过1,那么就可以降低树的高度,从而提高搜索效率。
AVL:两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis名字的缩写
1.2:AVL树
AVL树本质上:一棵高度平衡的二叉搜索树
1.3:AVL树的分类
1. 空树
2. 具有以下性质的二叉搜索树:
a. 左右子树都是AVL树
b. 左右子树的高度差的绝对值不超过1 (AVL树也叫做二叉平衡树)
1.4:平衡因子(balance factor 简称:bf)
bf = 左右子树高度差
AVL树每个节点的平衡因子绝对值不超过1: bf = (0、±1)
2. 实现AVL树
2.1:AVL树节点的定义
public class AVLTree {
//AVL节点的内部类
public static class TreeNode{
public int val;
public int bf;//balance factor 平衡因子
public TreeNode left;
public TreeNode right;
public TreeNode parent;
TreeNode(int val){
this.val=val;
}
}
public TreeNode root;
}
2.2:AVL树插入节点操作:
AVL树本质上是一棵高度平衡的二叉搜索树,所以插入一个节点的步骤:
1.按照二叉搜索树的方式插入节点
2.更新每个节点的平衡因子
3.对不平衡的子树进行调整,使其符合AVL树的性质
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(val>cur.val){
parent=cur;
cur=cur.right;
} else if (val<cur.val) {
parent=cur;
cur=cur.left;
}else {
return false;
}
}
//cur==null 插入节点
if(val>parent.val){
parent.right=node;
} else{
parent.left=node;
}
node.parent=parent;
cur=node;
//调节平衡因子
while(parent!=null){
//先看是插在了左子树还是右子树
if(cur==parent.right){
//右 右树高度增加 bf++
parent.bf++;
}else{
//左 左树高度增加 bf--
parent.bf--;
}
//检测当前的平衡因子
if(parent.bf==0){//平衡
break;
} else if (parent.bf==1||parent.bf==-1) {//继续向上平衡
cur=parent;
parent=cur.parent;
} else { //bf==2 不平衡 旋转
if(parent.bf==2){//右子树高 降低右子树的高度
if(cur.bf==1){ //插入到了较高右子树的右侧
//左旋
rotateLeft(parent);
}else { //cur.bf==-1 插入到了较高右子树的左侧
//先右旋再左旋
rotateRL(parent);
}
} else { //左子树高 降低左子树的高度
if(cur.bf==-1){//插入到了较高左子树的左侧
//右旋
rotateRight(parent);
}else { //cir.bf==1 插入到了较高左子树的右侧
//先左旋再右旋
rotateLR(parent);
}
}
//上述代码走完 平衡
break;
}
}
return true;
}
插入一个节点后,需要更新parent的平衡因子:
插入在了parent的右子节点的位置,parent的 bf ++
插入在了parent的左子节点的位置,parent的 bf --
检查插入节点后parent的平衡因子:有三种情况 0 、±1、±2
1. 如果是0:插入之前parent的平衡因子为正负1,插入之后调整为0,满足AVL树的性质,插入成功;
2. 如果是±1:插入之前parent的平衡因子一定为0,插入后被调整为±1,当前子树高度增加,需要继续向上更新
3. 如果是±2:parent的平衡因子不符合AVL树的性质,需要进行旋转
2.3:AVL树的旋转
在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。
旋转的分类:
左单旋:新节点插入到较高右子树的右侧(右右型)
右单旋:新节点插入到较高左子树的左侧(左左型)
左右双旋:新节点插入到了较高左子树的右侧
右左双旋:新节点插入到了较高右子树的左侧
1. 左单旋:
新节点插入到较高右子树的右侧,需要降低右子树的高度
步骤:
1. 将bf = 2的节点向左下方旋转
2. 将其右节点的左节点作为其右节点
3. 其成为其右节点的左节点
4. 更新平衡因子
代码:
//左单旋
private void rotateLeft(TreeNode parent){
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
parent.right=subRL;
subR.left=parent;
if(subRL!=null){
subRL.parent=parent;
}
TreeNode pParent=parent.parent;
parent.parent=subR;
if(pParent==root){
root=subR;
root.parent=null;
}else{
if(pParent.left==parent){
pParent.left=subR;
}else{
pParent.right=subR;
}
subR.parent=pParent;
}
subR.bf=0;
parent.bf=0;
}
2. 右单旋
新节点插入到较高左子树的左侧,需要降低左子树的高度
步骤:
1. 将bf = -2的节点向右下方旋转
2. 将其左节点的右节点作为其左节点
3. 其成为其左节点的右节点
4. 更新平衡因子
代码:
//右单旋
private void rotateRight(TreeNode parent){
TreeNode subL=parent.left;
TreeNode subLR=subL.right;
parent.left=subLR;
subL.right=parent;
if(subLR!=null){ //subLR可能不存在
subLR.parent=parent;
}
//先记录 parent.parent
TreeNode pParent = parent.parent;
parent.parent=subL;
if(pParent==root){ // pParent 为根节点
root=subL;
root.parent=null;
}else{ //不是根节点
// 看parent是pParent的左节点还是右节点
if(pParent.left==parent){
pParent.left=subL;
}else{ //pParent.right==parent
pParent.right=subL;
}
subL.parent=pParent;
}
//调节平衡因子
subL.bf=0;
parent.bf=0;
}
3. 左右双旋
新节点插入到了较高左子树的右侧
步骤:
1. 先对parent的左子树进行左旋
2. 再对整棵树进行右旋
3. 更新平衡因子
代码:
//左右双旋
private void rotateLR(TreeNode parent){
TreeNode subL=parent.left;
TreeNode subLR=subL.right;
int bf= subLR.bf;
rotateLeft(parent.left);
rotateRight(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;
}
}
4. 右左双旋
新节点插入到了较高右子树的左侧
步骤:
1. 先对parent的右子树进行右旋
2. 再对整棵树进行左旋
3. 更新平衡因子
代码:
//右左双旋
private void rotateRL(TreeNode parent){
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
int bf=subRL.bf;
rotateRight(parent.right);
rotateLeft(parent);
if(bf==1){
parent.bf=-1;
subR.bf=0;
subRL.bf=0;
}else if(bf == -1){
parent.bf=0;
subR.bf=0;
subRL.bf=-1;
}
}
总结
新节点插入后,假设以parent为根的子树不平衡:
parent与其较高子树节点的平衡因子时同号时单旋转,异号时双旋转。
旋转完成后,原parent为根的子树个高度降低,已经平衡,不需要再向上更新。
3. AVL树的验证
AVL树本质上是一棵高度平衡的二叉搜索树,所以验证一棵AVL树步骤:
1. 先验证是二叉搜索树:中序遍历得到的序列有序
2. 在验证高度平衡:
a. 每个节点的平衡因子绝对值不超过1
b. 节点的平衡因子是否计算正确
//中序遍历
public void inOrder(TreeNode root){
if(root==null){
return;
}
inOrder(root.left);
System.out.println(root.val+" ");
inOrder(root.right);
}
//求二叉树高度
private 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;
}
//AVL树的验证
public boolean isBalance(TreeNode root){
if(root==null){
return true;
}
int leftH = height(root.left);
int rightH = height(root.right);
//检查平衡因子
if(rightH-leftH!=root.bf){
System.out.println(root.val+"平衡因子异常");
return false;
}
return Math.abs(leftH-rightH)<=1
&& isBalance(root.left)
&& isBalance(root.right);
}
4. AVL树性能分析
一棵N个节点的AVL树:
高度:logN
查询的时间复杂度:O(logN)
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即O(logN) ;
但AVL树每次插入或删除节点都要通过旋转对结构进行调整,旋转的次数比较多,所以不适合频繁插入和删除数据;
因此:如果需要 一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。