二叉树改良版——AVL树

发布于:2024-10-12 ⋅ 阅读:(134) ⋅ 点赞:(0)

为什么说是“改良”,其实标题的二叉树指的是搜索二叉树,它虽然可以缩短查找的效率,但如果数据已经有序或接近有序的话二叉树就会退化成单支树,这样查找元素的话反而会效率低下。因此,为了解决这个问题,AVL树就得以出现,它的特征是:当向二叉搜索树插入新结点后,如果能保证每个结点的左右子树高度差的绝对值不超过1(调整后的),即可降低树的高度,进而减少搜索频率,提高效率。

一、AVL树简介

一棵AVL树具有以下性质:1.它的左右子树都是AVL树 2.左右子树高度之差(简称平衡因子)的绝对值不超过1。但后续我们为了方便研究,平衡因子通常是右子树高度-左子树高度。

那么如果我要插入一个数据,那会影响哪些结点的平衡因子呢?答案是其部分祖先,为什是部分呢?插入后,平衡因子会更新,若插入右子树,平衡因子++,插入在左子树,平衡因子--(其父节点的平衡因子变化规律),但是再向上找其祖先的平衡因子就不一定会变化,若父节点只有右子树,此时将数据插入左子树,那么其祖先的平衡因子不会变化。(因为其子树的高度没有变化)所以我们说是影响部分祖先。

我们来列举以下情况来分析,什么情况下需要向上更新祖先平衡因子什么时候又不需要。

假设我们现在插入了一个新结点后,其父节点parent平衡因子为0,1/-1,2/-2

第一种情况,说明我插入的位置一定是父节点左右子树中缺少的那个位置(若parent有左(右)那么一定插入的是右(左))此时parent的子树高度没变化,故不需要找祖先。

第二种情况,说明插入前的parent一定是0(不可能是2/-2,因为这样的情况就不满足AVL树的定义了),故插入后parent会多了左(右)子树,高度也会变化,需要找祖先。

第三种情况,说明parent更新前是1/-1,若其有左(右)子树的话会继续插入到其左(右)子树,高度也会增加,也需要找祖先。但此时已经违反了AVL的规则了,需要进行旋转,我们稍后会用代码实现。

我们先进行其结点的封装,因为AVL树的本质也是二叉搜索树,所以我们用之前的模板即可,这里还需要加工一下,用一个新的容器——map,map的本质虽然也是二叉搜索树,但它每个结点的存放是key-value类型(他们被一个pair的类封装又重命名为first和second)(类似于查字典的功能,一一对应),与其相似的还有一个容器set,它的底层就是我们熟悉的二叉搜索树了(有关set和map的封装作者会在下篇或下下篇总结)

接下来,我们看看平衡因子的更新

while (parent)
{
	if (cur == parent->_left)
		parent->_bf--;
	else
		parent->_bf++;
	if (parent->_bf == 0)
	{
		break;//第一种情况
	}
	else if (parent->_bf == 1 || parent->_bf == -1)
	{
		cur=parent;
		parent = parent->_parent;//向上找祖先
	}
	else if (parent->_bf == 2 || parent->_bf == -2)
	{
		//不平衡了,需要旋转
	}
	else
	{
		assert(false);
	}
}

注意:这段代码是放在结点插入的函数中且是在插入操作之后进行的,因为平衡因子改变的原因就是进行了插入操作。

二、AVL树的旋转

AVL树的旋转分为四类

1.右右插入——左单旋

我们来看下面这个情景

我们给几个数据命名一下:30——parent,60——subR,b——subRL,那么左单旋的思路就是:subRL变成parent的右,parent变成subR的左,subR变成parent,但实际代码实现时需要考虑很多情况,比如subR为空时情况又是怎样的。

代码如下:

void rotatel(Node*parent)
{
  Node*subR=parent->_right;
  Node*subRL=subR->_left;
   parent->_right=subRL;
    if(subRL)//如果不为空才能调整其父节点
     subRL->_parent=parent;

  Node*Parentparent=parent->_parent;//记录根结点的父节点,因为不确定调整的是整棵树还是一部分子树
    subR->left=parent;
  parent->_parent=subR;//每次调整后别忘调整其父节点
 if(Parentparent==nullptr)//说明调整的是整棵树,那么新的根就是subR
{
_root=subR;
subR->_parent=nullptr;//别忘置空,因为此时subR父节点还是parent
}
else//调整的是子树
{
   if(parent==Parentparent->_left)
   {
     Parentparent->_left=subR;
   }
   else
   {
     Parentparent->_right=subR;
   }
   subR->_parent=Parentparent;
}
parent->_bf=subR->_bf=0;
}

 
  

2.左左插入——右单旋 (左单旋的镜像)

代码与左单选是类似的,在此我们直接呈现代码

void rotater(Node*parent)
{
 Node*subL=parent->_left;
 Node*subLR=subL->_right;
  parent->_left=subLR;
   if(subLR)
     subLR->_parent=parent;

  Node*Parentparent=parent->_parent;
  subL->_right=parent;
  parent->_parent=subL;
 if(Parentparent==nullptr)
{
  _root=subL;
   subL->_parent=nullptr;
}
else
  {
   if(parent==Parentparent->left)
    {
     Parentparent->_left=subL;
    }
    else
    {
      Parentparent->_right=subL;
    }
     subL->_parent=Parentparent;
  }
parent->_bf=subL->_bf=0;
}

有了两个旋转的函数,我们就可以初步完善之前的插入代码了,我们选择摘取前面的一部分填写

else if (parent->_bf == 2 || parent->_bf == -2)
	{
		//不平衡了,需要旋转
        //利用平衡因子判断旋转方式,默认右-左
       if(parent->_bf == 2&&cur->_bf==1) //左旋
          rotatel(parent);
       else if(parent->_bf == -2&&cur->_bf==-1)//右旋
          rotater(parent);	
    }

接下来我们就要介绍一些稍微复杂的情况了

3.右左双旋——新节点插入较高右子树的左侧

我们假设以下情景:(注:下面说的所有 ab高度相同,cd高度相同,但ab比cd高一层)

如果abc均为空,d为70,此时如果把数据插入到d的左或右都满足上面的左单旋实现平衡,但如果我插入的位置是b或者c呢?如果通过左单旋进行旋转,发现其并不能达到平衡的效果。我们引进一个新的旋转方式——右左双旋。

我们先看最简单一只情况,假设此时abcd均为空,在90左插入60.(按前面的道理我们知道,无论是左旋还是右旋都无法平衡)接下来的操作:60变成30的右边,90变成60的右边,此时我们发现进行这种操作后情况符合左单旋的情况了,所以在此基础上再来一次左单旋即可(如果在左边同理),第一步的操作我们也叫右单旋(右左双旋中的右单旋)。

上面最简单的情况是不足以说明右单旋的原理的,看下面的情况,假设abcd均存在(bc是新插入的数据,如果插入在d的左右就符合左单旋的情形了,故插在60左右),右单旋的原理就是:60变成30的右,90变成60的右,c变成90的左,其他不变。至此,右单旋结束

下一步,左单旋,结果如下:

其实我们发现,在进行右左双旋之后,如果以60为中心,30为左,90为右,那么该操作的结果就是把中间的左给了左的右,中间的右给了右的左。

幸运的是,这里我们可以复用前面的函数,套一层就好了,代码如下:

void rotaterl(Node*parent)
{
    rotater(parent->_right);//右旋以90为中心
    rotatel(parent);
}

但这个双旋的难处在于其平衡因子的调整,我们假设现在树的情形是,abcd均存在且还要再bc间选择一个地方插入。经分析,插在b和c对于30和90来说两次的平衡因子是不一样的,所以我们需要先知道其插入的位置才能确定平衡因子。很明显,看60那个位置的平衡因子就可以了。

但,如果插入的数据是60呢(也就是abcd均不存在),看结果我们知道最后的平衡因子都是0,所以这个情况比较简单。因此我们还要分情况讨论。

插入后,如果subRL(60)的平衡因子是1说明插在了c下,-1就是b下,0就是60本身是插入的数据。

上面的代码只是实现了结点关系的转换并没有对平衡因子进行修改,所以我们需要完善一下代码。

void rotaterl(Node*parent)
{
    Node*subR=parent->_right;
    Node*subRL=subR->_left;
    int bf=subRL->_bf;
 //在旋转之前进行变量的存储以便于对后续平衡因子的修改
    rotater(parent->_right);//右旋以90为中心
    rotatel(parent);
    if(bf==0)
    {
      subR->_bf=0;
      subRL->_bf=0;
      parent->_bf=0;
    }
    else if(bf==1)//插在了c侧
    {
      parent->_bf=-1;
      subR->_bf=0;
      subRL->_bf=0;
    }
    else if(bf==-1)//插在了b侧
    {
     parent->_bf=0;
     subR->_bf=1;
     subRL->_bf=0;
    }
    else 
    {
     assert(false);
    }
}

4.左右双旋—新节点插入较高左子树的右侧

与右左双旋类似,这边直接上代码加旋转流程图

void rotatelr(Node*parent)
{
  Node*subL=parent->_left;
  Node*subLR=subL->_right;
  int bf=subLR->_bf;
    rotatel(parent->left);
    rotater(parent);
  if(bf==0)
  { 
   parent->_bf=0;
   subL->_bf=0;
   subLR->_bf=0;
  }
  else if(bf==-1)
  {
   parent->_bf=1;
   subL->_bf=0;
   subLR->_bf=0;
  }
  else if(bf==1)
  {
   parent->_bf=0;
   subL->_bf=-1;
   subLR->_bf=0;
  }
  else
  {
   assert(false);
  }
}  

三、AVL树的验证

上面我们完善了AVL树,但其外表本质还是二叉搜索树,如果按中序遍历输出结果上和正常的二叉搜索树没有区别,无法验证其内部的高度是否平衡。因此我们需要验证其平衡性进而区分出他们的区别。很明显,区分二者最明显的方法就是利用平衡因子。

int _height(Node*root)
{ 
   if(root==nullptr)
       return 0;
   int leftheight=_height(root->_left);
   int rightheight=_height(root->_right);

return leftheight>rightheight ? lefthright+1 : rightheight+1;
}

bool _isbalancetree(Node*root)
{
    if(root==nullptr)//空树也是AVL树
      return true;
    int leftheight=_height(root->_left);//利用计算高度函数获得当前高度
    int rightheight=_height(root->_right);
    int diff=rightheight-leftheight;//高度差,即根结点的平衡因子
// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
 // Root平衡因子的绝对值超过1,则一定不是AVL树
    if(abs(diff)>=2||diff!=root->_bf)//abs是绝对值函数,解决了高度差正负的问题
       return false;
//如果左右子树都是AVL树,该树一定是AVL树
    return _isbalancetree(root->_left)&&_isbalancetree(root->_right);
}

除此之外,我们也可以写一个函数记录AVL树中的结点个数,还是递归思想,这里就不展示了。(需要注意的是,由于上面的函数均需要传结点,但是private成员不可访问,所以我们要在外套一层函数传参才能使用)

至于AVL树的删除作者在此就不多讲解了,思路虽然和插入操作的原理类似,但其情况分析其实要比插入复杂的多,涉及到按搜索树的规则删除,更新平衡因子,其异常后还要旋转处理。

以上就是AVL树的一些基本内容了,其中比较重要的是插入以及旋转的原理和平衡因子的调整,建议小伙伴们反复关顾哦,请留下你宝贵的三连再走,这对我更新的动力真的很大(doge)。


网站公告

今日签到

点亮在社区的每一天
去签到