【数据结构与算法学习笔记-AVL树平衡二叉树】

AVL树是一种自平衡二叉搜索树,确保任意节点的两个子树高度差不超过1,从而保持高效的查找效率。文章介绍了AVL树的基本概念、平衡因子、旋转操作(左旋LL、右旋RR、特殊情况LR和RL旋转)以及插入操作的时间复杂度分析。通过旋转操作,AVL树能够在插入节点后自动调整,维持O(logn)的查找性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文为学习笔记,感兴趣的读者可在MOOC中搜索《数据结构与算法Python版》或阅读《数据结构(C语言版)》(严蔚敏)
目录链接:https://blog.csdn.net/floating_heart/article/details/123991211

4.3 AVL树

4.3.1 初识AVL树

根据4.2 BST的介绍(链接:)我们可知,BST的性能和二叉搜索树的深度紧密相关:当key值分布极端时,大量的数据集中在单一分支上,此时树的深度最大,性能最差;当key值分布均匀,数据均匀分布在不同分支上时,树的深度最小,性能最佳。

为了保证BST的搜索效率,让树深度降低,后续学者提出了“自平衡二叉树(Self-Balancing Binary Search Tree)”。故名思意,自平衡二叉树能够自己调节节点的分布使其保持“平衡”,进而维持 O ( l o g n ) O(logn) O(logn)的高效查询效率,常见的自平衡二叉树有AVL树和红黑树等,下面以AVL树为例,了解该结构自平衡的实现方法。

AVL树是最先发明的自平衡查找树,在AVL树中任何节点的两个子树的高度最大差别为1,所以也被称为高度平衡树。它具有以下特点:

  1. 本身是一棵二叉搜索树;
  2. 带有平衡条件:每个节点的左右子树的高度差的绝对值(平衡因子)最多为1。

4.3.2 AVL树判断平衡的标准:平衡因子

在AVL树的实现中,需要对每个结点跟踪“平衡因子balance factor”参数。
平衡因子是根据结点的左右子树的高度来定义的,确切地说,是左右子树的高度差:
b a l a n c e F a c t o r = h e i g h t ( l e f t S u b T r e e ) − h e i g h t ( r i g h t S u b T r e e ) balanceFactor = height(leftSubTree)-height(rightSubTree) balanceFactor=height(leftSubTree)height(rightSubTree)
如果平衡因子大于0,称为“左重left-heavy”;小于0,称为“右重right-heavy”;等于0,则称作平衡。

如果一个二叉查找树中每个节点的平衡因子都在-1,0,1之间,则把这个二叉搜索树称为平衡树
在平衡树的操作过程中(如增加、删除等操作),节点的平衡因子会发生变化,如下图所示:

在这里插入图片描述

这种影响可能会随着父节点到根节点的路径一直传递上去,直到:

  • 传递到根节点为止;
  • 或者某个父节点平衡因子被调整到0,不再影响上层节点的平衡因子为止(如下图所示)。

在这里插入图片描述

AVLTree的大部分功能和BST类似,我们修改BST来适应AVL的要求,BST的内容可见4.2章节。

首先,我们改造TreeNode类,加入balanceFactor属性作为平衡因子,新节点添加在末尾,所以平衡因子默认为0。

class TreeNode:
  def __init__(self,key,val,left=None,right=None,parent=None) -> None:
      self.key = key # 键值
      self.payload = val # 数据项
      self.leftChild = left # 左子节点
      self.rightChild = right # 右子节点
      self.parent = parent # 父节点
      self.balanceFactor = 0 # 平衡因子

根据平衡因子影响的方式,我们在加入一个新节点之后,只需逐层向上传播影响即可。为此,我们适当改造_put()函数(插入非根节点的新节点的函数),加入updateBalance()方法从底到顶更新平衡因子。

  # put中_put方法
  def _put(self,key,val,currentNode):
    if key < currentNode.key: # key比currentNode.key小,递归左子树
      if currentNode.hasLeftChild():
        self._put(key,val,currentNode.leftChild)
      else:
        currentNode.leftChild = TreeNode(key,val,parent=currentNode)
        self.updateBalance(currentNode.leftChild) # 插入节点后,调整平衡因子
    else: # key比currentNode.key大,递归右子树
      if  currentNode.hasRightChild():
        self._put(key,val,currentNode.rightChild)
      else:
        currentNode.rightChild = TreeNode(key,val,parent=currentNode)
        self.updateBalance(currentNode.rightChild) # 插入节点后,调整平衡因子

updateBalance()方法更新平衡因子,在平衡因子超标(大于1或小于-1)时,调用rebalance()方法调节平衡具体如下:

  • 如果本节点平衡因子超标,调用rebalance()方法调节平衡,调节之后影响中止,调节的方法在下节介绍;
  • 本节点平衡因子没有超标,则将影响传递到parent节点:是左子树parent节点平衡因子+1,是右子树parent节点平衡因子-1,然后在parent节点平衡因子不为0的情况下向上递归
  # 更新平衡因子
  def updateBalance(self,node):
    # 调节平衡
    if node.balanceFactor > 1 or node.balanceFactor < -1:
      self.rebalance(node) 
      return
    # 影响向上传播
    if node.parent != None:
      if node.isLeftChild():
        node.parent.balanceFactor += 1
      elif node.isRightChild():
        node.parent.balanceFactor -= 1
      
      if node.parent.balanceFactor != 0:
        self.updateBalance(node.parent)

4.3.3 AVL树调节平衡的方法:旋转

updateBalance()函数中对于平衡因子超标的节点调用了rebalance()方法用于重新调节平衡并中断影响,中断影响的条件前文已述:该结点的平衡因子被调节为0。那么如何实现平衡调节就是这一部分的重点。

AVL树重新平衡的主要手段为:将不平衡的子树进行旋转rotation——视“左重”或者“右重”进行不同方向的旋转,同时更新相关父节点引用,更新旋转后被影响节点的平衡因子。

左旋:LL

对于“右重”的子树需要进行左旋,以下图子树A为例:

  1. 首先将子节点B提升为子树的根,将旧节点A作为新根节点B的左子节点;

  2. 如果新根节点原来有左子节点,则将此节点设置为A的右子节点(A的右子节点一定有空,且符合BST性质)。

  3. 各个节点的平衡因子随之变动。

在这里插入图片描述

右旋:RR

对于“左重”的子树需要进行右旋,以下图子树E为例:

  1. 首先将子节点C提升为子树的根,将旧根节点E作为新根节点C的右子节点;
  2. 新根节点C原有的右子节点D作为旧根节点E的左子节点;
  3. 各个节点平衡因子随之变动。

在这里插入图片描述

特殊情况:LR和RL旋转

存在一些特殊情况无法用单纯的“左旋”或“右旋”解决,如下图所示:
在这里插入图片描述

上图中的子树,左旋之后会变成“左重”,再右旋后又变回了“右重”。可以采取以下方案:

  • 在左旋之前检查右子节点的平衡因子,如果右子节点“左重”的话,先对右子节点进行右旋,再实施原来的左旋(RL);
  • 在右旋之前检查左子节点的平衡因子,如果左子节点“右重”的话,先对左子节点进行左旋,再实施原来的右旋(LR)。

如此操作,上面的例子就可以解决:

在这里插入图片描述

旋转操作对平衡因子的影响:

上述旋转操作中都提到了“各个节点平衡因子随之变动”,此处计算下旋转中平衡因子的变动方式以方便后续旋转操作的实现。

左旋和右旋是向对称的一组操作,平衡因子的变动相同。

  • 左旋操作平衡因子变化的情况如下:

在这里插入图片描述

(符号说明:旧节点代表左图变换前的节点,新节点代表右图变换后的节点,如旧B代表作图根节点B的平衡因子;h代表子树高度,如hA代表子树A的高度。)

首先,子树A、C、E在变换过程中不涉及内部子节点的变化,hA、hC、hE不变,故主要为B和D节点平衡因子的变化。

左旋B的变化:新B-旧B

新 B = h A − h C 新B=hA-hC B=hAhC
旧 B = h A − 旧 h D = h A − ( 1 + m a x ( h C , h E ) ) 旧B = hA - 旧hD=hA-(1+max(hC,hE)) B=hAhD=hA(1+max(hC,hE))

新 B − 旧 B = h A − h C − h A + ( 1 + m a x ( h C , h E ) ) = 1 + m a x ( h C , h E ) − h C 新B-旧B=hA-hC-hA+(1+max(hC,hE))=1+max(hC,hE)-hC BB=hAhChA+(1+max(hC,hE))=1+max(hC,hE)hC
新 B − 旧 B = 1 + m a x ( h C , h E ) − h C = 1 + m a x ( h C − h C , h E − h C ) = 1 + m a x ( 0 , − 旧 D ) 新B-旧B=1+max(hC,hE)-hC=1+max(hC-hC,hE-hC)=1+max(0,-旧D) BB=1+max(hC,hE)hC=1+max(hChC,hEhC)=1+max(0,D)

故: 新 B = 旧 B + 1 − m i n ( 0 , 旧 D ) 新B=旧B+1-min(0,旧D) B=B+1min(0,D)

左旋D的变化:新D-旧D

新 D = 新 h B − h E = ( 1 + m a x ( h A , h C ) ) − h E 新D=新hB-hE=(1+max(hA,hC))-hE D=hBhE=(1+max(hA,hC))hE
旧 D = h C − h E 旧D=hC-hE D=hChE

新 D − 旧 D = ( 1 + m a x ( h A , h C ) ) − h E − ( h C − h E ) = 1 + m a x ( h A , h C ) − h C 新D-旧D=(1+max(hA,hC))-hE-(hC-hE)=1+max(hA,hC)-hC DD=(1+max(hA,hC))hE(hChE)=1+max(hA,hC)hC
新 D − 旧 D = 1 + m a x ( h A − h C , 0 ) = 1 + m a x ( 新 B , 0 ) 新D-旧D=1+max(hA-hC,0)=1+max(新B,0) DD=1+max(hAhC,0)=1+max(B,0)

故: 新 D = 旧 D + 1 + m a x ( 新 B , 0 ) 新D=旧D+1+max(新B,0) D=D+1+max(B,0)

  • 右旋操作与之类似,以上图镜像对称的子树为例,直接给出结论:

右旋旧根节点变化: 新 B = 旧 B − 1 − m a x ( 旧 D , 0 ) 新B=旧B-1-max(旧D,0) B=B1max(D,0)
右旋新根节点变化: 新 D = 旧 D − 1 + m i n ( 新 B , 0 ) 新D=旧D-1+min(新B,0) D=D1+min(B,0)

根据以上分析,我们大概认识了旋转操作必要的组成部分,构建代码则变得比较容易。
下面,我们从rebalance()开始,构建旋转操作的代码:

  # rebalance:再平衡,判断旋转方式进行响应旋转操作
  def rebalance(self,node):
    if node.balanceFactor < 0: # 右重-左旋
      if node.rightChild.balance.balanceFactor > 0: # 右子节点左重-RL旋转
        self.rotateRight(node.rightChild)
        self.rotateLeft(node)
      else: # LL旋转
        self.rotateLeft(node)
    elif node.balanceFactor > 0: # 左重-右旋
      if node.leftChild.balanceFactor < 0: # 左子节点右重-LR旋转
        self.rotateLeft(node.leftChild)
        self.rotateRight(node)
      else: # RR旋转
        self.rotateRight(node)

相应的左旋和右旋操作如下:

  # 左旋操作
  def rotateLeft(self,rotRoot):
    # 提出新根节点
    newRoot = rotRoot.rightChild
    # 旋转操作:节点之间的连接的建立-newRoot和rotRoot之间的关系已知,可以从此处入手
    # 旧根节点向下连接
    rotRoot.rightChild = newRoot.leftChild
    if newRoot.leftChild != None:
      newRoot.leftChild.parent = rotRoot
    # 新根节点向上连接
    newRoot.parent = rotRoot.parent
    if rotRoot.isRoot():
      self.root = newRoot
    else:
      if rotRoot.isLeftChild():
        rotRoot.parent.leftChild = newRoot
      else:
        rotRoot.parent.rightChild = newRoot
    # 旧根节点向上连接
    newRoot.leftChild = rotRoot
    rotRoot.parent = newRoot
    # 平衡因子调整
    rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor,0)
    newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor,0)  
  # 右旋操作
  def rotateRight(self,rotRoot):
      newRoot = rotRoot.leftChild
      rotRoot.leftChild = newRoot.rightChild
      if newRoot.rightChild != None:
          newRoot.rightChild.parent = rotRoot
      if rotRoot.isRoot():
          self.root = newRoot
      else:
          if rotRoot.isLeftChild():
              rotRoot.parent.leftChild = newRoot
          else:
              rotRoot.parent.rightChild = newRoot
      newRoot.rightChild = rotRoot
      rotRoot.parent = newRoot
      rotRoot.balanceFactor = rotRoot.balanceFactor - 1 - max(newRoot.balanceFactor,0)
      newRoot.balanceFactor = newRoot.balanceFactor - 1 + min(rotRoot.balanceFactor,0)

4.3.4 AVL树时间复杂度分析

put操作的性能:

AVL树的put操作分为三个部分:

  • 向下遍历查找插入位置,这一操作和AVL树的高度有关,复杂度最多为 O ( l o g n ) O(log n) O(logn)
  • 向上遍历调节平衡因子,这一操作同样和AVL树的高度有关,在最差的情况下,复杂度为 O ( l o g n ) O(logn) O(logn)
  • 向上遍历的过程中,如果触发了旋转操作,重新平衡最多需要两次旋转,复杂度为 O ( 1 ) O(1) O(1)

所以,AVL树put操作的时间复杂度于BST相同,为 O ( l o g n ) O(logn) O(logn)

get操作的性能:

与BST相同,get操作的性能和树的高度有关,由于AVL树总能保持平衡,所以其复杂度最多为 O ( l o g n ) O(logn) O(logn)

为什么说AVL树的高度和数据规模之间为 l o g n logn logn倍的关系:

我们首先以最差的形式来考虑AVL树的高度,即数据规模(N)尽量小而树的高度(h)尽量高的情况,如下图所示:

在这里插入图片描述

h = 1 , N = 1 h=1,N=1 h=1,N=1
h = 2 , N = 2 = 1 + 1 h=2,N=2=1+1 h=2,N=2=1+1
h = 3 , N = 4 = 1 + 1 + 2 h=3,N=4=1+1+2 h=3,N=4=1+1+2
h = 4 , N = 7 = 1 + 2 + 4 h=4,N=7=1+2+4 h=4,N=7=1+2+4

陈斌老师讲解中,给出通式: N h = 1 + N h − 1 + N n − 2 N_h=1+N_{h-1}+N_{n-2} Nh=1+Nh1+Nn2 (汗!笔者没看出此通式,只能听老师的话)
此通式很接近斐波那契数列!(这点笔者看出来了!哈哈!)

h h h N h N_h Nh和斐波那契数列 F i F_i Fi,列出如下:

h / i h/i h/i012345678
N h N_h Nh0124712203354
F i F_i Fi01123581321

可以明显看出 N h N_h Nh F i F_i Fi的关系如下:

F 0 = 0 , F 1 = 1 N h = 1 + N h − 1 + N h − 2 F_0=0,F_1=1\quad N_h=1+N_{h-1}+N_{h-2} F0=0,F1=1Nh=1+Nh1+Nh2
F i = F i − 1 + F i − 2   f o r   a l l   i ≥ 2 N h = F h + 2 − 1 F_i=F_{i-1}+F_{i-2}\, for\: all\:i≥2\quad N_h=F_{h+2}-1 Fi=Fi1+Fi2foralli2Nh=Fh+21

斐波那契数列中, F i / F i − 1 F_i/F_{i-1} Fi/Fi1趋近于黄金分割 ϕ = 1 + 5 2 \phi=\frac{1+\sqrt5}{2} ϕ=21+5
所以斐波那契数列的通式接近于 F i = ϕ i / 5 F_i=\phi^i/\sqrt5 Fi=ϕi/5

于此,可以得到 N h N_h Nh的通式: N h = ϕ h + 2 5 − 1 N_h=\frac{\phi^{h+2}}{\sqrt5}-1 Nh=5 ϕh+21

通式中存在 h h h N h N_h Nh的关系,化简后得到:(得到的公式和陈斌老师的不同,不知道哪里出错了)
h = log ⁡ ( N h + 1 ) + 1 2 log ⁡ 5 + 2 log ⁡ ϕ log ⁡ ϕ h=\frac{\log(N_h+1)+\frac{1}{2}\log5+2\log\phi}{\log\phi} h=logϕlog(Nh+1)+21log5+2logϕ
所以,AVL树的高度和数据量之间为 log ⁡ n \log n logn倍的关系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值