数据结构与算法(python):平衡二叉查找树(AVL)

参考自 MOOC数据结构与算法Python版

一、AVL树的定义

能够在key插入时一直保持平衡的二叉查找树: AVL树
(AVL是发明者的名字缩写)

  • 利用AVL树实现ADT Map, 基本上与BST的实现相同,不同之处仅在于二叉树的生成与维护过程
  • AVL树的实现中, 需要对每个节点跟踪“平衡因子balance factor”参数
  • 平衡因子是根据节点的左右子树的高度来定义的, 确切地说, 是左右子树高度差
    – 平衡因子 > 0,称为“左重left-heavy”,
    – 平衡因子 < 0,称为“右重right-heavy”,
    – 平衡因子 = 0,称作平衡。
    如果一个二叉查找树中每个节点的平衡因子都在-1, 0, 1之间, 则把这个二叉搜索
    树称为平衡树

二、AVL树的性能O(log n)

我们来分析AVL树最差情形下的性能:即平衡因子为1或者-1
下图列出平衡因子为1的“左重”AVL树,树的高度从1开始,来看看问题规模(总节点数N)和比对次数(树的高度h)之间的关系如何?

在这里插入图片描述
观察上图h=1~4时, 总节点数N的变化:
h= 1, N= 1
h= 2, N= 2= 1+ 1
h= 3, N= 4= 1+ 1+ 2
h= 4, N= 7= 1+ 2+ 4
规律: N h = 1 + N h − 1 + N h − 2 N_h=1+N_{h-1}+N_{h-2} Nh=1+Nh1+Nh2 接近斐波那契数列

1. 定义斐波那契数列 F i F_i Fi
F 0 = 0 F 1 = 1 F i = F i − 1 + F i − 2 F_0=0\\F_1 = 1\\F_i=F_{i-1}+F_{i-2} F0=0F1=1Fi=Fi1+Fi2
由于 N h = 1 + N h − 1 + N h − 2 N_h=1+N_{h-1}+N_{h-2} Nh=1+Nh1+Nh2 可得
N h = F h + 2 − 1 , h ≥ 1 N_h=F_{h+2}-1,h\ge1 Nh=Fh+21,h1

2. 斐波那契数列的性质: F i / F i − 1 F_i/F_{i-1} Fi/Fi1趋向于黄金分割 Φ \Phi Φ
Φ = 1 + 5 2 \Phi =\frac{1+\sqrt{5}}{2} Φ=21+5
可以得到 F i F_i Fi的通式
F i = Φ i 5 {{F}_{i}}=\frac{{{\Phi }^{i}}}{\sqrt{5}} Fi=5 Φi
3.
在这里插入图片描述

最多搜索次数h和规模N的关系, 可以说AVL树的搜索时间复杂度为 O ( l o g n ) O(log n) O(logn)

三、AVL树的Python实现

既然AVL平衡树确实能够改进BST树的性能, 避免退化情形,我们来看看向AVL树插入一个新key, 如
何才能保持AVL树的平衡性质。

  1. 首先, 作为BST, 新key必定以叶节点形式插入到AVL树中
  2. 叶节点的平衡因子是0, 其本身无需重新平衡,但会影响其父节点的平衡因子
  3. 这种影响可能随着其父节点到根节点的路径一直传递上去, 直到
    传递到根节点为止;某个父节点平衡因子被调整到0,不再影响上层节点的平衡因子为止
    在这里插入图片描述

3.1 put方法

相比二叉查找树,只需重新定义_put方法
以刚刚插入的新节点作为基础,去调整它的平衡因子

    def _put(self, key, val, currentNode):
        # 如果key比currentNode小,那么_put到左子树
        if key < currentNode.key:
            # 但如果没有左子树,那么key就成为左子节点
            if currentNode.left:
                self._put(key, val, currentNode.left)  # 递归查找空左子树
            else:
                currentNode.left = TreeNode(key, val, parent=currentNode)
                self.updataBalance(currentNode.left) #调整因子
        # 如果key比currentNode大,那么_put到右子树
        else:
            # 但如果没有右子树,那么key就成为右子节点
            if currentNode.right:
                self._put(key, val, currentNode.right)  # 递归右子树
            else:
                currentNode.right = TreeNode(key, val, parent=currentNode)
                self.updataBalance(currentNode.right) #调整因子

3.2 UpdateBalance方法

    def UpdateBalance(self,node):
        if node.balanceFactor > 1 or node.balanceFactor < -1:
            self.rebalance(node)
            return
        if node.parent:
            if node.isLeftChild():
                node.parent.balancefactor += 1
            elif node.isRightChild():
                node.parent.balancefactor -= 1
            if node.parent.balancefactor != 0:
                self.updataBalance(node.parent)

3.3 rebalance重新平衡

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

  • 如图, 是一个“右重”子树A的左旋转(并保持BST性质)

    • 将右子节点B提升为子树的根,将旧根节点A作为新根节点B的左子节点;
    • 如果新根节点B原来有左子节点,则将此节点设置为A的右子节点(A的右子节点一定有空)
      在这里插入图片描述
  • 更复杂一些的情况:如图的“左重”子树右旋转

    • 旋转后,新根节点将旧根节点作为右子节点,但是新根节点原来已有右子节点,需要将原有的右子节点重新定位!
    • 原有的右子节点D改到旧根节点E的左子节点同样, E的左子节点在旋转后一定有空

    在这里插入图片描述

3.4 rotateLeft 左旋

    def rotateLeft(self,rotRoot):
        newRoot = rotRoot.right #新根节点是旧根节点的右子节点
        #把旧根节点的右子节点指向新根节点的左子节点
        rotRoot.right = newRoot.left
        if newRoot.left: #新根节点有左节点
            newRoot.left.parent = rotRoot#将新根的左节点指向旧根结点
        newRoot.parent = rotRoot.parent 
        #如果旧根节点是树的根,那么应该确定新的树根
        if rotRoot.isRoot():
            self.root = newRoot
        else: 
            if rotRoot.isLeftChild(): #若旧根节点是左子节点
                rotRoot.parent.left = newRoot #旧根节点的左子节点指向新根节点
            else:
                rotRoot.parent.right = newRoot
        newRoot.left = rotRoot
        rotRoot.parent = newRoot
        #仅有两个结点需要调整因子
        rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0)
        newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0)

在这里插入图片描述

3.5 如何调整平衡因子

在这里插入图片描述在这里插入图片描述

3.6 AVL树的实现: 更复杂的情形

下图的“右重”子树, 单纯的左旋转无法实现平衡

在这里插入图片描述
所以, 在左旋转之前检查右子节点的因子

  • 如果右子节点“左重”的话,先对它进行右旋转再实施原来的左旋转

同样, 在右旋转之前检查左子节点的因子

  • 如果左子节点“右重”的话,先对它进行左旋转再实施原来的右旋转

3.6 rebalance

    def rebalance(self,node):
        if node.balanceFactor < 0: #右重需要左旋
            if node.right.balanceFactor > 0:
                # 右子节点左重先右旋
                self.rotateRight(node.right) 
                self.rotateLeft(node)
            else: self.rotateLeft(node)
        elif node.balanceFactor > 0: #左重需要右旋
            if node.left.balanceFactor < 0:
                #左子节点右重先左旋
                self.rotateLeft(node.left)
                self.rotateRight(node)
            else:
                self.rotateRight(node)

经过复杂的put方法, AVL树始终维持平衡, get方法也始终保持O(log n)高性能

  • 需要插入的新节点是叶节点,更新其所有父节点和祖先节点的代价最多为O(log n)
  • 如果插入的新节点引发了不平衡,重新平衡最多需要2次旋转,但旋转的代价与问题规模无关,是常数O(1)
  • 所以整个put方法的时间复杂度还是O(log n)

四、ADT Map的实现方法小结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值