Binary Search Tree

二叉搜索树

前面介绍了二叉树,下面继续介绍接触最多的二叉搜索树BST,对于一颗BST的节点,若存在左子树则左子树值均小于节点值,若存在右子树则右子树值均大于节点值,其中序遍历恰好为所有值的一个排序,所以又叫做二叉排序树。
利用二叉树的这一性质在树中查找某一值时可以递归到子树,从而大大降低搜索复杂度,在平衡的情况下可以达到logn级别。同样,也可以用于排序,基于原二叉树继续实现BST

class BST(BTreeNode):
    """二叉搜索树
    对于树中任一节点r,其左/右子树均小于/大于r,现假设左子树小于r,右子树大于等于r"""

match

在二叉树查找一个值,根据BST特点可以递归实现

    def match(self, x):
        """从当前节点开始搜索,找第一个匹配的,未找到则返回None"""
        if x == self.data:
            return self
        if x < self.data and self.l_child is not None:
            return self.l_child.match(x)
        if x >= self.data and self.r_child is not None:
            return self.r_child.match(x)

insert

要插入一个值时,从根节点开始,类似查找的过程,小于节点值则插入左子树,否则插入右子树,直到插入为空时执行插入操作

    def insert(self, x, types=True):
        """插入的时候不能破坏这一结构, 时间复杂度为O(h),h为树高度"""
        if types and self.parent is not None:
            # 先找到根节点
            return self.parent.insert(x)
        if x < self.data:
            if self.l_child is None:
                self.l_child = BST(x, parent=self)
            else:
                self.l_child.insert(x, False)
        else:
            if self.r_child is None:
                self.r_child = BST(x, parent=self)
            else:
                self.r_child.insert(x, False)

remove

要删除一个节点时,相应的子树应该填补其位置并不能破坏树的结构,所以就要讨论子树是否存在

    def remove(self, x):
        at = self.match(x)
        if at is None:
            return False
        self._remove_at(at)
        return True

    def _remove_at(self, node):
        """移除某个节点并不破坏结构"""
        left = node.l_child
        right = node.r_child

        # 左右子树均为空,直接移除
        if left is None and right is None:
            if node.parent is None:
                node.data = None
            else:
                p = node.parent
                if p.l_child is not None and p.l_child.data == node.data:
                    p.l_child = None
                else:
                    p.r_child = None

        # 左右子树存在一个,用子树节点代替自己
        elif left is None:
            node.data = right.data
            node.r_child = right.r_child
            node.l_child = right.l_child
        elif right is None:
            node.data = left.data
            node.r_child = left.r_child
            node.l_child = left.l_child

        # 左右子树均存在则用右子树最小值代替自己并把自己移除
        else:
            while right.l_child is not None:
                right = right.l_child

            node.data = right.data
            self._remove_at(right)

AVL

BST查找性能已将比普通BT好多了,但有一个问题不能被忽视,如果原序列已经是有序的在构建BST时,就会一直往一个方向插入从而退化成单链表,查找性能肯定就大大降低了。
为了解决这一问题,可以简单把序列打乱顺序再执行插入操作,但最后结果就完全靠天意了,虽然可以有效较少树高,但还存在一个问题,再以后的插入或删除操作无法保证不会退化成单链表。
所以提出了平衡二叉搜索树,它要求其结构类似完全二叉树从而保证树高在log2n范围内。
而AVL就是其中的一种,属于高度平衡二叉树,它近似完全平衡了,并且其能通过简单的旋转操作恢复树的平衡。
AVL比BST多了一个限定条件,其引入了平衡因子,即左右子树的高度差,并且保证平衡因子在[-1,1]范围内,从而保证不会出现像BST往一边倒的情况。
基于BST,继续实现AVL

class AVL(BST):
    """高度平衡二叉搜索树,BST改进版,由于BST在给定序列基本有序时会退化成链表
    多了个限定条件,即任一节点左右子树高度差值绝对值<=1"""
    def __init__(self, data=None, l_child=None, r_child=None, parent=None):
        super().__init__(data, l_child, r_child, parent)
        self.balance_f = 0  # 平衡因子即左右子树高度差

旋转

旋转操作不是AVL特有的,只是其能通过旋转恢复平衡,所谓旋转就是在保证其中序遍历结果不变的条件下交换孩子节点与父节点位置从而可以增加或较少左右子树高度差
右旋时左子树必须存在并把左子树往上提,原先节点成为了左子树的右节点,而原先左子树的右节点成为了原先节点的左节点,其实只需要始终记住中序遍历结果不能变这个前提,这些节点的转换就很容易了
顺时针旋转,即右旋
左旋类似
左旋
下面是用代码实现这一过程

    def _zig(self):
        """顺时针旋转, 保持中序遍历结果一样"""
        # 把左节点作为新的父节点
        cur = self.l_child

        # 根据中序遍历交换子树并更新父指针
        cur.parent, self.parent = self.parent, cur
        self.l_child = cur.r_child
        if self.l_child is not None:
            self.l_child.parent = self
        cur.r_child = self

        # 更新平衡因子
        cur_ll = 0 if cur.l_child is None else len(cur.l_child)
        cur.balance_f = cur_ll - len(cur.r_child)
        self_ll = 0 if self.l_child is None else len(self.l_child)
        self_rl = 0 if self.r_child is None else len(self.r_child)
        self.balance_f = self_ll - self_rl

        # # 如果是根节点则返回新的根节点,否则原地修改
        if cur.parent is None:
            return cur
        else:
            if cur.parent.l_child is not None and cur.parent.l_child.data == self.data:
                cur.parent.l_child = cur
            else:
                cur.parent.r_child = cur

逆时针zag旋转与上边对称

插入

考察插入过程,插入之前树肯定是平衡的,插入后如果插入位置平衡因子为0则肯定是平衡的,为±1时需要考虑其会影响到父节点,所以需要向上更新平衡因子,中途遇到±2时肯定就失衡了,需要调整,为0时就不会向上传播了。
根据左右子树平衡因子以及失衡节点平衡因子可以概括为4种情况分成两组互相对称,仔细考察平衡因子更新过程,其实无非就是一次旋转能不能解决的问题,先来概括下平衡因子变化情况

  • 往非叶子节点中插入值时,当前节点平衡因子变为0,所以没有失衡,其祖先节点也一定不会失衡
  • 往一个叶子节点中插入值时,其平衡因子必然变成1或-1,此时就要考虑其对父节点影响了,此时可以称为平衡因子的向上传播,可以概括为:

在这里插入图片描述

代码完全是基于这一理解写的,自己多画画就理解了,并且还需注意到一个事实调整平衡后其祖先节点平衡因子不会变:

    def insert(self, x, types=True):
        # 返回插入节点的父节点
        cur = self._insert(x, types)

        # 如果平衡因子为1或-1则有可能改变某个上层节点平衡因子不在规定范围
        # 从当前节点向上更新节点的平衡因子,直到有+-2失衡或0就不会改变父节点的平衡因子了
        while cur.balance_f != 0 and cur.parent is not None:
            tmp = cur
            cur = cur.parent

            # 左孩子节点则平衡因子加1,否则为右孩子节点
            if cur.l_child is not None and cur.l_child.data == tmp.data:
                cur.balance_f += 1
            else:
                cur.balance_f -= 1

            # 失衡需作调整,且调整后不会改变上层节点平衡因子
            if cur.balance_f == 2 or cur.balance_f == -2:
                break

        # 没失衡
        if cur.balance_f in (0, -1, 1):
            return

        # 失衡4种情况,两对称
        # 由左子树的平衡因子为1引起的当前平衡因子为2失衡,此时易知是左子树的左子树较深,经过一次右旋即可
        if cur.balance_f == 2 and cur.l_child is not None and cur.l_child.balance_f == 1:
            res = cur._zig()

        # 由左子树的平衡因子为-1引起的当前平衡因子为2失衡,左旋
        elif cur.balance_f == -2 and cur.r_child is not None and cur.r_child.balance_f == -1:
            res = cur._zag()

        # 由左子树的平衡因子为-1引起的当前平衡因子为2失衡,此时需旋转两次
        elif cur.balance_f == 2 and cur.l_child is not None and cur.l_child.balance_f == -1:
            cur.l_child._zag()
            res = cur._zig()

        # 和上一种对称
        else:
            cur.r_child._zig()
            res = cur._zag()

        return res

_insert方法和BST一样就最后多了个更新平衡因子

remove

删除一个节点时比插入情况就复杂一些了,局部调整平衡后可能整体并未平衡,还未实现

RBT

红黑树,也是一种平衡二叉树,条件比AVL宽一些,所以更容易平衡,其考察的是左右子树高度不相差两倍即可,这也能保证不退化成链表
红黑树每个节点引入一个颜色属性,区分红黑节点,需满足以下条件:

  • 根节点始终为黑色
  • 外部节点均为黑色
  • 红色节点的孩子节点必为黑色
  • 从根节点到任一外部节点遇到的黑节点数目相等

其中外部节点为人为在叶子节点处添加的n+1个假想节点,起初看这个定义确实不好理解,我最先想到的是那我完全不用红节点呀, 那好像只有满二叉树才能满足那些条件。引入两种颜色正是它的巧妙之处,从以上条件可以得到红色节点均为内部节点,且其父节点和左右孩子节点必然存在
代码还未实现。。。。。

测试结果

if __name__ == '__main__':
    lst = list(range(20))
    random.shuffle(lst)
    bst = BST.load_from_list(lst)
    avl = AVL.load_from_list(lst)
    print('bst:', len(bst), repr(bst))
    print('bst-remove:', bst.remove(10), len(bst), list(travel_in(bst)))
    print('avl:', len(avl), repr(avl))

out:
bst: 8 {"data": 8, "root": true, "left": {"data": 0, "right": {"data": 4, "left": {"data": 1, "right": {"da      ...
bst-remove: True 8 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19]
avl: 5 {"data": 8, "root": true, "left": {"data": 4, "left": {"data": 1, "left": {"data": 0}, "right": {"da      ...

完整代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值