平衡二叉树python实现(AVL树)

AVL树是一种自平衡二叉搜索树,由苏联数学家 Georgy Adelson-Velsky 和 Landis 在 1962 年提出,以他们的名字首字母命名。AVL树的特点是它通过额外的平衡信息和旋转操作来保证树的高度平衡,从而确保树的查找、插入和删除等操作的时间复杂度在最坏情况下也能达到O(log n)。

1.平衡二叉树(AVL树)

1. 定义

AVL树是带有平衡条件的二叉搜索树:

  • 每个节点最多有两个子节点(左子节点和右子节点)。

  • 对于树中的每个节点,其左子树和右子树的高度差(称为平衡因子(balance factor))的绝对值不超过1。即平衡因子 ∈ {−1, 0, 1}。

  • 左子树和右子树也都是AVL树。

2. 性质

AVL树具有以下性质:

  • 平衡因子:每个节点的平衡因子定义为该节点左子树的高度减去右子树的高度。平衡因子只能是-1、0或1(在下文代码中令一个节点的平衡因子:如果右子树更深为正,左子树更深为负)。

  • 自平衡:当插入或删除节点导致树不平衡时,AVL树会通过旋转操作自动恢复平衡。

  • 高度平衡:AVL树的高度始终是O(log n),其中n是节点的数量。

3. 操作

插入

在AVL树中插入一个节点后,只有从插入节点到根节点的路径上的节点的平衡可能被改变,需要找出第一个破坏了平衡条件的节点,称为P,该节点两棵子树的高度差为2。如果不平衡,需要通过旋转操作来恢复平衡。旋转操作有四种类型:

  • LL旋转(左左右转):当插入节点到P的左孩子的左子树时,进行单右旋。

    def rotate_right(self, p, c):   # 右旋
        s2 = c.rchild
        p.lchild = s2
        if s2: # 如果s2不是空的
            s2.parent = c
        # 连接p和c
        c.rchild = p
        p.parent = c
        # 更新balance factor
        p.bf = 0
        c.bf = 0
        return c   # 返回旋转之后的根节点
  • RR旋转(右右左旋):当插入节点到P的右孩子的右子树时,进行单左旋。

        

    def rotate_left(self, p, c):   # 左旋
        s2 = c.lchild
        p.rchild = s2
        if s2:   # s2不是空的
            s2.parent = p
        # 连接c和p
        c.lchild = p
        p.parent = c
        # 更新balance factor
        p.bf = 0
        c.bf = 0
        return c  # 返回旋转之后的根节点
  • LR旋转(左右旋转):当插入节点到左子树的右子树时,先对左子树进行左旋,再对根节点进行右旋。(左旋相当于对C和G进行左旋,左旋之后的状态应该是s2是C的右子树,G在P的左孩子位置,然后再对P和G进行右旋)

    def rotate_left_right(self, p, c): # 先左旋后右旋
        g = c.rchild
        # 对g和c进行左旋
        s2 = g.lchild
        c.rchild = s2
        if s2:
            s2.parent = c
        # 连接g和c
        g.lchild = c
        c.parent = g
        # 对g和p进行右旋
        s3 = g.rchild
        p.lchild = s3
        if s3:
            s3.parent = p
        # 连接p和g
        g.rchild = p
        p.parent = g
        # 更新balance factor
        if g.bf > 0:  # 插入的是s3位置
            p.bf = 0
            c.bf = -1
        elif g.bf < 0:  # 插入的是s2位置
            c.bf = 0
            p.bf = 1
        else:   # 插入的位置是g
            p.bf = 0
            c.bf = 0
        g.bf = 0
        return g   # 返回旋转之后的根节点
  • RL旋转(右左旋转):当插入节点到P的右孩子的左子树时,先对右子树进行右旋,再对根节点进行左旋。(右旋相当于对G和C进行右旋,旋转之后的状态应该是s3为C的右子树,G在P的右子树位置,然后再对P和G进行左旋)

    def rotate_right_left(self, p, c):  # 先右旋后左旋
        # 对于g和c进行右旋
        g = c.lchild
        s3 = g.rchild
        c.lchild = s3
        if s3:
            s3.parent = c
        # 连接g和c
        g.rchild = c
        c.parent = g
        # 进行了一次右旋之后,g应该在p的右孩子的位置
        # 继续对p和g进行左旋
        s2 = g.lchild
        p.rchild = s2
        if s2:
            s2.parent = p
        # 连接p和g
        g.lchild = p
        p.parent = g
        # 更新balance factor
        if g.bf > 0:   # 相当于插入的位置为g的右孩子位置
            c.bf = 0
            p.bf = -1
        elif g.bf < 0:  # 相当于插入的位置为g的左孩子位置
            p.bf = 0
            c.bf = 1
        else:   # 插入的位置是g
            p.bf = 0
            c.bf = 0
        g.bf = 0
        return g  # 返回旋转之后的根节点

    def rotate_left_right(self, p, c): # 先左旋后右旋
        g = c.rchild
        # 对g和c进行左旋
        s2 = g.lchild
        c.rchild = s2
        if s2:
            s2.parent = c
        # 连接g和c
        g.lchild = c
        c.parent = g
        # 对g和p进行右旋
        s3 = g.rchild
        p.lchild = s3
        if s3:
            s3.parent = p
        # 连接p和g
        g.rchild = p
        p.parent = g
        # 更新balance factor
        if g.bf > 0:  # 插入的是s3位置
            p.bf = 0
            c.bf = -1
        elif g.bf < 0:  # 插入的是s2位置
            c.bf = 0
            p.bf = 1
        else:   # 插入的位置是g
            p.bf = 0
            c.bf = 0
        g.bf = 0
        return g   # 返回旋转之后的根节点

在插入一个元素之后,其父亲节点的平衡因子会发生变化,并且向上传递(传递给其父亲的父亲),在传递的过程中直到某个节点的平衡因子为零,更新结束,如果有一个节点的平衡因子变成2或者-2,说明该节点的平衡被破坏,需要进行旋转操作。

4.代码实现

结合操作部分的图示对代码可以进行更好的理解。

# 平衡二叉树

# 节点定义
class AVLNode:
    def __init__(self, data):
        self.data = data
        self.lchild = None  # 左孩子
        self.rchild = None   # 右孩子
        self.parent = None   # 父节点
        self.bf = 0   # 平衡因子

class AVLTree:
    def __init__(self, li=None):
        self.root = None  # 根节点
        if li:  # 列表非空
            for val in li:
                self.insert_no_rec(val)

    def rotate_left(self, p, c):   # 左旋
        s2 = c.lchild
        p.rchild = s2
        if s2:   # s2不是空的
            s2.parent = p
        # 连接c和p
        c.lchild = p
        p.parent = c
        # 更新balance factor
        p.bf = 0
        c.bf = 0
        return c  # 返回旋转之后的根节点

    def rotate_right(self, p, c):   # 右旋
        s2 = c.rchild
        p.lchild = s2
        if s2: # 如果s2不是空的
            s2.parent = c
        # 连接p和c
        c.rchild = p
        p.parent = c
        # 更新balance factor
        p.bf = 0
        c.bf = 0
        return c   # 返回旋转之后的根节点

    def rotate_right_left(self, p, c):  # 先右旋后左旋
        # 对于g和c进行右旋
        g = c.lchild
        s3 = g.rchild
        c.lchild = s3
        if s3:
            s3.parent = c
        # 连接g和c
        g.rchild = c
        c.parent = g
        # 进行了一次右旋之后,g应该在p的右孩子的位置
        # 继续对p和g进行左旋
        s2 = g.lchild
        p.rchild = s2
        if s2:
            s2.parent = p
        # 连接p和g
        g.lchild = p
        p.parent = g
        # 更新balance factor
        if g.bf > 0:   # 相当于插入的位置为g的右孩子位置
            c.bf = 0
            p.bf = -1
        elif g.bf < 0:  # 相当于插入的位置为g的左孩子位置
            p.bf = 0
            c.bf = 1
        else:   # 插入的位置是g
            p.bf = 0
            c.bf = 0
        g.bf = 0
        return g  # 返回旋转之后的根节点

    def rotate_left_right(self, p, c): # 先左旋后右旋
        g = c.rchild
        # 对g和c进行左旋
        s2 = g.lchild
        c.rchild = s2
        if s2:
            s2.parent = c
        # 连接g和c
        g.lchild = c
        c.parent = g
        # 对g和p进行右旋
        s3 = g.rchild
        p.lchild = s3
        if s3:
            s3.parent = p
        # 连接p和g
        g.rchild = p
        p.parent = g
        # 更新balance factor
        if g.bf > 0:  # 插入的是s3位置
            p.bf = 0
            c.bf = -1
        elif g.bf < 0:  # 插入的是s2位置
            c.bf = 0
            p.bf = 1
        else:   # 插入的位置是g
            p.bf = 0
            c.bf = 0
        g.bf = 0
        return g   # 返回旋转之后的根节点


    def insert_no_rec(self, val):
        # 第一步,先进行插入操作
        p = self.root
        if not p: # 没有根节点
            self.root = AVLNode(val)
            return
        while True:  # 根节点存在,进入循环
            if val < p.data:
                if p.lchild:  # 如果p的左孩子存在
                    p = p.lchild
                else:  # p的左孩子不存在
                    p.lchild = AVLNode(val)
                    p.lchild.parent = p
                    node = p.lchild   # 存储的是插入的节点
                    break   # 执行完一次插入操作之后结束循环,继续进行更新平衡因子的操作
            elif val > p.data:
                if p.rchild:  # 如果p的左孩子存在
                    p = p.rchild
                else:  # p的左孩子不存在
                    p.rchild = AVLNode(val)
                    p.rchild.parent = p
                    node = p.rchild   # 存储插入节点
                    break  # 同理,结束循环
            else:  # 如果val和某个节点的数据相等
                return   # 在这里也可以在节点中多定义一个用于重复元素计数的属性,遇到相等的情况,进行+1

            # 第二步,更新balance factor
        while node.parent:
            if node.parent.lchild == node: # 传递是从node.parent的左子树来的,也就是左子树更沉,传递过程中的节点的balance factor需要进行-1
                if node.parent.bf < 0:  # 原来的node.parent.bf = -1,更新之后变成-2
                # 有一个问题,为什么原来的会node.parent.bf = -1,第一次学的时候感觉插入节点的父亲应该是零
                # 其实结合整体来看,node是会继续往上进行更新的,第一次把node.parent.bf更新成1或者-1之后,会继续执行
                # node = node.parent的操作,直到遇到旋转,或者在某次循环中的node.parent.bf=0,结束循环。
                    # 继续进行判断旋转操作
                    #if node.parent.parent:
                    g = node.parent.parent  # 如果要进行旋转操作,node.parent会变化,所以要记录node.parent.parent用来连接旋转子树后新的根节点
                    #else:
                        #g = None
                    x = node.parent   # 记录旋转之前的node.parent用来判断旋转之后的根节点连接g的左孩子还是右孩子
                    if node.bf > 0: # 说明插入的位置是node.parent的左孩子的右子树左右
                        n = self.rotate_left_right(node.parent, node)  # 记录旋转之后的子树的新的根节点
                    else:  # 插入的位置是node.parent的左孩子的左子树
                        n = self.rotate_right(node.parent, node)
                elif node.parent.bf > 0:  # 原来的node.parent.bf = 1,更新之后变成0
                    node.parent.bf = 0
                    break
                else:  # 原来node.parent.bf = 0,更新之后变成-1
                    node.parent.bf = -1
                    node = node.parent
                    continue
            else:  # 传递是从右子树来的,循环过程中的节点的balance factor需要进行+1
                if node.parent.bf > 0:  # 原来的node.parent.bf = 1,更新之后变成2
                    # 判断旋转操作
                    #if node.parent.parent:
                    g = node.parent.parent
                    #else:
                        #g = None
                    x = node.parent   # 记录旋转之前的node.parent用来判断旋转之后的根节点连接g的左孩子还是右孩子
                    if node.bf < 0: # 说明插入的位置是node.parent的右孩子的左子树,右左
                        n = self.rotate_right_left(node.parent, node)
                    else: # 说明插入的位置是node.parent的右孩子的右子树,左旋
                        n = self.rotate_left(node.parent, node)
                elif node.parent.bf < 0:
                    node.parent.bf = 0
                    break
                else:
                    node.parent.bf = 1
                    node = node.parent
                    continue

            # 连接旋转后的子树
            n.parent = g
            if g:  # g不是空
                if x == g.lchild:
                    g.lchild = n
                else:
                    g.rchild = n
                break
            else:
                self.root = n
                break

    # 中序遍历
    def in_order(self, root):
        if root:
            self.in_order(root.lchild)
            print(root.data, end=', ')
            self.in_order(root.rchild)

    # 前序遍历
    def pre_order(self, root):
        if root:  # 如果有数据
            print(root.data, end=', ')
            self.pre_order(root.lchild)
            self.pre_order(root.rchild)







2.AVL树与二叉搜索树对比

1.平衡性

  • 二叉搜索树:在理想情况下(树完全平衡),其高度为O(logn),但在最坏情况下(如插入的序列是有序的)会退化为链表,导致操作性能下降。

  • AVL树:始终保持平衡,无论插入和删除操作如何进行,都能保证树的高度为O(logn),从而保证操作的高效性。

2.操作性能

  • 查找性能:理想情况下两者查找性能相同,时间复杂度为O(logn)。但在最坏情况下,二叉搜索树查找性能较差,时间复杂度为O(n),而AVL树查找性能仍然保持在O(logn)。

  • 插入性能:理想情况下两者插入性能相同,时间复杂度为O(logn)。但在最坏情况下,二叉搜索树插入性能较差,时间复杂度为O(n),而AVL树插入性能仍然保持在O(logn)。

  • 删除性能:理想情况下两者删除性能相同,时间复杂度为O(logn)。但在最坏情况下,二叉搜索树删除性能较差,时间复杂度为O(n),而AVL树删除性能仍然保持在O(logn)。

3.应用场景

  • 二叉搜索树:适用于数据量较小或插入/删除操作较频繁的场景,因为其结构简单,维护成本低。

  • AVL树:适用于需要高效查找的场景,如数据库索引、字典等,因为其始终保持平衡,查找效率高。

4.实现复杂度

  • 二叉搜索树:实现相对简单,插入和删除操作不需要额外的平衡调整。

  • AVL树:实现较为复杂,需要维护每个节点的平衡因子,并在插入和删除操作后进行旋转调整以保持平衡。

3. 总结

AVL树是一种高效的自平衡二叉搜索树,通过旋转操作保证树的高度平衡,从而确保各种操作的时间复杂度在最坏情况下也能达到O(log n)。虽然AVL树的插入和删除操作比普通的二叉搜索树复杂,但在需要频繁查找的场景中,AVL树的性能优势非常明显。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值