AVL平衡二叉树

本文结合python数据结构一书
https://facert.gitbooks.io/python-data-structure-cn/6.%E6%A0%91%E5%92%8C%E6%A0%91%E7%9A%84%E7%AE%97%E6%B3%95/6.17.AVL%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E5%AE%9E%E7%8E%B0/

简介

相信各位有计算机基础的话,是对二叉树这种结构有一定了解的。二叉树表示简单,寻找方便,但若插入顺序不一,则形成的结构也不一样,从而很可能导致空间的浪费。今天我们要介绍的是一种二叉树的改进版,它通过平衡因子以及旋转的操作来维护整个二叉树,使其尽可能保持在一个平衡状态

平衡因子

平衡因子在AVL是一个比较重要的概念。我们将平衡因子定义为左子树的高度 减去 右子树的高度,在平衡二叉树中,各个节点的平衡因子均能保持在0,-1, 1这个范围内。

如果平衡因子大于0,我们说这个节点是左重的,因为左子树高度大于右子树

同理,如果平衡因子小于0,我们说这个节点是右重的,因为右子树高度大于左子树

在这里插入图片描述
我们看根节点,它的平衡因子就是左子树的高度(1),减去右子树高度(3)得到 -2

添加节点

平衡二叉树是基于二叉树改进的,引入了平衡因子后,我们添加节点的方法也要做一些小改变

首先新加入的节点平衡因子都为0,因此我们只需针对父节点来更新平衡因子

分以下几种情况

  1. 新节点是父节点的右节点,则将父节点平衡因子减1,因为右子树高度增加了
  2. 新节点是父节点的左节点,则将父节点平衡因子加1,因为左子树高度增加了

这个调用关系可以一直递归调用到祖先,直到树的根
因此我们仍需列出递归的两种基本情况
3. 递归调用已经到了树的根
4. 父节点的平衡因子已经调节到0

    def _put(self,key,val,currentNode):
        """
        重写_put方法
        :param key:
        :param val:
        :param currentNode:
        :return:
        """
        if key < currentNode.key:
            # 如果key小于当前节点的key,则要插入到左子树
            if currentNode.hasLeftChild():
                # 如果当前节点已经有左子树了,则递归调用,继续找
                self._put(key, val, currentNode.leftChild)
            else:
                # 如果当前节点没有左子树,则创建一个TreeNode放置
                currentNode.leftChild = TreeNode(key, val, parent=currentNode)
                # 调用updateBalance方法更新平衡因子
                self.updateBalance(currentNode.leftChild)
        else:
            # 如果key大于当前节点的key,则要插入到右子树
            if currentNode.hasRightChild():
                # 如果当前节点已经有右子树了,则递归调用,继续找
                self._put(key, val, currentNode.rightChild)
            else:
                # 如果当前节点没有左子树,则创建一个TreeNode放置
                currentNode.rightChild = TreeNode(key, val, parent=currentNode)
                # 调用updateBalance方法更新平衡因子
                self.updateBalance(currentNode.rightChild)

接下来是updateBalance方法的代码

    def updateBalance(self, node):
        if node.balanceFactor > 1 or node.balanceFactor < -1:
            # 如果平衡因子不在范围内,则调用rebalance方法
            self.rebalance(node)
            return
        if node.parent != None:
            if node.isLeftChild():
                # 如果当前插入节点是左孩子
                # 则父节点的平衡因子加1
                node.parent.balanceFactor += 1
            elif node.isRightChild():
                # 如果当前插入节点是右孩子
                # 则父节点的平衡因子减1
                node.parent.balanceFactor -= 1

            if node.parent.balanceFactor != 0:
                # 如果执行完后,父节点平衡因子不为0
                # 则递归调用updateBalance方法
                self.updateBalance(node.parent)

旋转操作

当平衡因子不在-1 0 1范围内,我们需要对当前节点做一个重新平衡
而重新平衡涉及到两个操作,分别是左旋转右旋转

旋转的目的是保证平衡因子在-1 0 1范围内,同时不改变二叉树的值大小顺序

在这里插入图片描述
上图是一个左旋的例子,我们可以看到A的平衡因子为-2,我们对其做一个左旋转,A就转到B的左边,而B此时变为根,同时ABC之间的大小顺序仍符合二叉树的顺序

左旋转

左旋转的具体操作有如下3个

  1. 将右子树作为根
  2. 将旧根作为新根的左孩子
  3. 若新根原本就有左孩子,则将新根的左孩子作为旧根的右孩子

第三个规则可能有点绕,我们知道新根是旧根的右子树,因此新根的孩子都是大于旧根的,所以将其作为旧根的右孩子

我们看以下这副图
在这里插入图片描述
这里附上左旋转的代码

    def rotateLeft(self, rotRoot):
        """
        左旋
        1. 将右孩子作为根
        2. 旧根移到左子
        3. 若新根已经有一个左孩子,则将其作为旧根的右孩子
        :param rotRoot:
        :return:
        """
        newRoot = rotRoot.rightChild
        rotRoot.rightChild = newRoot.leftChild
        if newRoot.leftChild != None:
            # 若新根已经有一个左孩子,调整其指针,因为此时它是作为旧根的右孩子,因此parent调整为旧根
            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)

右旋转

右旋转的步骤如下

  1. 将左孩子作为新的根
  2. 将旧根作为新根的右子树
  3. 如果新根已经有一个右孩子,那么让其成为旧根左孩子

第三个步骤逻辑跟左旋转的逻辑是很相似的,新根是旧根的左子树,因此新根及其节点都是小于旧根的,因此将新根的右孩子做为旧根左孩子

下面的图演示的是右旋转
在这里插入图片描述
这里附上右旋转的代码

    def rotateRight(self, rotRoot):
        """
        右旋
        1. 将左孩子作为根
        2. 将旧根作为新根的右子树
        3. 如果新根已经有一个右孩子了,则作为新根的左孩子
        :param rotRoot:
        :return:
        """
        newRoot = rotRoot.leftChild
        rotRoot.leftChild = newRoot.rightChild
        if newRoot.rightChild != None:
            newRoot.rightChild.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.rightChild = rotRoot
        rotRoot.parent = newRoot
        rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0)
        newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0)

右旋转的代码和左旋转是对称的,除了我们调换的顺序之外,我们还要维护一下调换后节点的父节点和孩子节点的指针指向,如果不调整是会引发错误的

最后两行是调整旧根和新根的平衡因子
这需要一定的公式推导

我们以一个左旋转为例子
在这里插入图片描述
在这里插入图片描述
newBal表示旋转后的平衡因子
oldBal表示旋转前的平衡因子
H则代表各个节点的高度
我们就推出来这行代码

rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0)

接下来我们看另一行代码的推导,原理是类似的
在这里插入图片描述
由此得到了后面一行代码

        newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0)

重新平衡

只有在一定规则下进行左旋转和右旋转才能平衡节点
重新平衡的规则如下

  1. 如果子树需要左旋转使其平衡,首先检查右子节点的平衡因子。 如果右孩子是重的,那么对右孩子做右旋转,然后是原来的左旋转。
  2. 如果子树需要右旋转使其平衡,首先检查左子节点的平衡因子。 如果左孩子是重的,那么对左孩子做左旋转,然后是原来的右旋转。

这里的重指的就是该节点的平衡因子不为0
下面我们看一个例子
在这里插入图片描述
我们需要平衡节点A,A是右重的,因此需要左旋转,那么我们根据规则1,它的右孩子C平衡因子是1,是重的,因此对C做右旋转,再对A做左旋转

下面是我们的代码

    def rebalance(self, node):
        if node.balanceFactor < 0:
            if node.rightChild.balanceFactor > 0:
                self.rotateRight(node.rightChild)
                self.rotateLeft(node)
            else:
                self.rotateLeft(node)
        elif node.balanceFactor > 0:
            if node.leftChild.balanceFactor > 0:
                self.rotateLeft(node.leftChild)
                self.rotateRight(node)
            else:
                self.rotateRight(node)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值