在Python系列中构建森林:红黑树

 

项目设置

遵循与Build the Forest in Series中的其他文章相同的样式和假设,该实现假定使用Python 3.9或更高版本。本文向我们的项目添加了两个模块:red_black_tree.py用于实现红黑树实现,test_red_black_tree.py用于其单元测试。添加这两个文件后,我们的项目布局变为以下内容:

 
<span style="color:#111111"><span style="color:#000000">forest-python
├── forest
│   ├── __init__.py
│   ├── binary_trees
│   │   ├── __init__.py
│   │   ├── binary_search_tree.py
│   │   ├── double_threaded_binary_tree.py
│   │   ├── red_black_tree.py
│   │   ├── single_threaded_binary_trees.py
│   │   └── traversal.py
│   └── tree_exceptions.py
└── tests
    ├── __init__.py
    ├── conftest.py
    ├── test_binary_search_tree.py
    ├── test_double_threaded_binary_tree.py
    ├── test_red_black_tree.py
    ├── test_single_threaded_binary_trees.py
    └── test_traversal.py</span></span>

 

什么是红黑树?

红黑树通过自身平衡能力来改变二叉搜索树,并且它的节点具有一个额外的属性:颜色,可以是红色或黑色。除二进制搜索树属性外,一棵红黑树还满足以下红黑树属性:

  • 每个节点都是红色或黑色
  • 根是黑色的
  • 每片叶子都是黑色的
  • 如果节点为红色,则其两个子节点均为黑色
  • 每个节点从节点到叶子的所有路径都包含相同数量的黑色节点

红黑树通过限制从节点到其后代叶子的任何简单路径上的节点颜色来确保没有路径比其他路径长两倍。换句话说,一棵红黑树大约是平衡的。

一棵典型的红黑树如下图所示。图片1

 

树数据结构的叶节点通常是指没有任何子节点的节点。但是,我们使用NIL表示红黑树的叶子,并且它们始终是黑色的。此外,叶节点不保存任何数据,主要用于维护红黑树属性。

我们使用黑色高度指示从节点到叶子的黑色节点数(如果该节点为黑色,则不包括该节点)。下图显示了每个节点的黑色高度。

图片2

每个节点旁边是该节点的黑色高度,叶节点(NIL)的黑色高度为零。

建造红黑树

就像我们在前面的树中所做的那样,本节将逐步介绍实现并讨论实现选择背后的一些想法。

由于所有叶子都是NIL,并且根节点的父节点也可以指向NIL,因此当我们实现一棵红黑树时,我们可以定义一个NIL节点,并使根节点的父节点和所有支持的节点指向NIL节点,NIL节点。因此,上一部分中显示的红黑树如下所示:

图片3

这种方式简化了实现并节省了空间(即,在实现中仅需要一个NIL节点实例)。

为简单起见,在本文的其余部分中将省略NIL节点,因此上面的红黑树将如下所示(但在实现中,NIL节点必须位于该树中;否则,它将违反红黑树-树属性)。

图片4

节点

红黑树节点类似于二叉搜索树节点,但还有一个属性-颜色。

图片5

由于颜色必须是红色或黑色,因此我们可以将其定义为枚举类。

Python
 
<span style="color:#111111"><span style="color:#000000"><span style="color:#0000ff">import</span> enum

<span style="color:#0000ff">class</span> Color(enum.Enum):
    RED = enum.auto()
    BLACK = enum.auto()</span></span>

为什么要使用枚举?

根据PEP-435,“枚举是绑定到唯一,恒定值的一组符号名称。在枚举中,可以通过标识比较这些值,并且可以迭代枚举本身。” 我们将color属性定义为一个枚举类是有道理的,因为它的值(红色或黑色)是恒定的,并且我们可以识别color属性并进行比较。同样,枚举类提供了更强大的类型安全性。如果不使用枚举,则可以将color属性定义为常量字符串。但是,类型检查工具(例如mypy)将无法检查值是color属性还是常规字符串。另一种选择是将color属性定义为常规类或数据类,如下所示:

Python
 
<span style="color:#111111"><span style="color:#000000"><span style="color:#339999">@dataclass</span>
<span style="color:#0000ff">class</span> Color:
    RED: str = <span style="color:#800080">"</span><span style="color:#800080">red"</span>
    BLACK: str = <span style="color:#800080">"</span><span style="color:#800080">black"</span></span></span>

这种方法仍然有一些缺点。例如,下划线类型仍然是字符串。我们可以将其与任何字符串进行比较。此外,一类是可变的。换句话说,我们可以在运行时修改与颜色定义矛盾的Color类。因此,使用枚举定义颜色属性最有意义。它增加了类型安全性并简化了实现。

红黑树节点

像其他二叉树节点一样,我们利用数据类来定义红黑树节点。

Python
 
<span style="color:#111111"><span style="color:#000000"><span style="color:#0000ff">from</span> dataclasses <span style="color:#0000ff">import</span> dataclass

<span style="color:#339999">@dataclass</span>
<span style="color:#0000ff">class</span> Node:
    <span style="color:#800080">"""</span><span style="color:#800080">Red-Black Tree non-leaf node definition."""</span>

    key: Any
    data: Any
    left: Union[<span style="color:#800080">"</span><span style="color:#800080">Node"</span>, Leaf] = Leaf()
    right: Union[<span style="color:#800080">"</span><span style="color:#800080">Node"</span>, Leaf] = Leaf()
    parent: Union[<span style="color:#800080">"</span><span style="color:#800080">Node"</span>, Leaf] = Leaf()
    color: Color = Color.RED</span></span>

叶节点

正如红黑树定义中提到的那样,我们使用NIL来指示叶节点,可以如下定义它。

Python
复制代码
<span style="color:#111111"><span style="color:#000000"><span style="color:#0000ff">from</span> dataclasses <span style="color:#0000ff">import</span> dataclass

<span style="color:#339999">@dataclass</span>
<span style="color:#0000ff">class</span> Leaf:
    color = Color.BLACK</span></span>

班级概况

像Build the Forest项目中的其他类型的二叉树一样,红黑树类具有相似的功能。

Python
收缩▲   复制代码
<span style="color:#111111"><span style="color:#000000"><span style="color:#0000ff">class</span> RBTree:

    <span style="color:#0000ff">def</span> __init__(self) -> <span style="color:#0000ff">None</span>:
        self._NIL: Leaf = Leaf()
        self.root: Union[Node, Leaf] = self._NIL

    <span style="color:#0000ff">def</span> __repr__(self) -> str:
        <span style="color:#800080">"""</span><span style="color:#800080">Provie the tree representation to visualize its layout."""</span>
        <span style="color:#0000ff">if</span> self.root:
            <span style="color:#0000ff">return</span> (
                f<span style="color:#800080">"</span><span style="color:#800080">{type(self)}, root={self.root}, "</span>
                f<span style="color:#800080">"</span><span style="color:#800080">tree_height={str(self.get_height(self.root))}"</span>
            )
        <span style="color:#0000ff">return</span> <span style="color:#800080">"</span><span style="color:#800080">empty tree"</span>

    <span style="color:#0000ff">def</span> search(self, key: Any) -> Optional[Node]:
        …

    <span style="color:#0000ff">def</span> insert(self, key: Any, data: Any) -> <span style="color:#0000ff">None</span>:
        …

    <span style="color:#0000ff">def</span> delete(self, key: Any) -> <span style="color:#0000ff">None</span>:
        …

    <span style="color:#339999">@staticmethod</span>
    <span style="color:#0000ff">def</span> get_leftmost(node: Node) -> Node:
        …

    <span style="color:#339999">@staticmethod</span>
    <span style="color:#0000ff">def</span> get_rightmost(node: Node) -> Node:
        …

    <span style="color:#339999">@staticmethod</span>
    <span style="color:#0000ff">def</span> get_successor(node: Node) -> Union[Node, Leaf]:
        …

    <span style="color:#339999">@staticmethod</span>
    <span style="color:#0000ff">def</span> get_predecessor(node: Node) -> Union[Node, Leaf]:
        …

    <span style="color:#339999">@staticmethod</span>
    <span style="color:#0000ff">def</span> get_height(node: Union[Leaf, Node]) -> int:
        …

    <span style="color:#0000ff">def</span> inorder_traverse(self) -> traversal.Pairs:
        …

    <span style="color:#0000ff">def</span> preorder_traverse(self) -> traversal.Pairs:
        …

    <span style="color:#0000ff">def</span> postorder_traverse(self) -> traversal.Pairs:
        …

    <span style="color:#0000ff">def</span> _transplant(
        self, deleting_node: Node, replacing_node: Union[Node, Leaf]
    ) -> <span style="color:#0000ff">None</span>:
        …

    <span style="color:#0000ff">def</span> _left_rotate(self, node_x: Node) -> <span style="color:#0000ff">None</span>:
        …

    <span style="color:#0000ff">def</span> _right_rotate(self, node_x: Node) -> <span style="color:#0000ff">None</span>:
        …

    <span style="color:#0000ff">def</span> _insert_fixup(self, fixing_node: Node) -> <span style="color:#0000ff">None</span>:
        …

    <span style="color:#0000ff">def</span> _delete_fixup(self, fixing_node: Union[Leaf, Node]) -> <span style="color:#0000ff">None</span>:
        …</span></span>

请注意,除了所有二叉树的通用方法外,RBTree类还有一些其他方法。_left_rotate() _right_rotate() _insert_fixup() ,和_delete_fixup()是辅助功能以保持插入和删除后红黑树属性。插入和删除操作均会修改树;因此,该操作可能会违反红黑树属性。这就是为什么我们需要这些功能。

叶子节点是NIL,因此二叉树遍历的遍历例程(使用None来确定是否到达叶子节点)不适用于RBTree类(需要使用Leaf来确定是否到达叶子节点)。因此,RBTree类需要提供其遍历例程。

由于插入操作会修改红黑树,因此结果可能会违反红黑树属性(对于删除也是如此)。为了恢复红黑树的属性,我们需要更改某些节点的颜色,并更新红黑树的结构。更新二叉树结构的方法称为旋转。解决违规的红黑树属性的技术来自算法导论,而红黑树插入的思想可以归纳为以下几点:

  1. 以与二进制搜索树插入相同的方式,以红色插入新节点:通过从根目录遍历树并比较新节点,找到正确的位置(即,新节点的父节点)以插入新节点。节点的密钥以及整个过程中的每个节点的密钥。
  2. 通过旋转和着色来修复损坏的红黑树属性。

由于新节点为红色,因此我们可以违反red-black-tree-property –如果一个节点为红色,则其两个子节点都为黑色,我们可以在插入后修复冲突。

我们可以通过与普通的二进制搜索树插入类似的方式来实现红黑树插入。

Python
复制代码
<span style="color:#111111"><span style="color:#000000"><span style="color:#0000ff">def</span> insert(self, key: Any, data: Any) -> <span style="color:#0000ff">None</span>:
    <span style="color:#008000"><em>#</em></span><span style="color:#008000"><em> Color the new node as red.</em></span>
    new_node = Node(key=key, data=data, color=Color.RED)
    parent: Union[Node, Leaf] = self._NIL
    current: Union[Node, Leaf] = self.root
    <span style="color:#0000ff">while</span> isinstance(current, Node):  <span style="color:#008000"><em>#</em></span><span style="color:#008000"><em> Look for the insert location</em></span>
        parent = current
        <span style="color:#0000ff">if</span> new_node.key < current.key:
            current = current.left
        <span style="color:#0000ff">elif</span> new_node.key > current.key:
            current = current.right
        <span style="color:#0000ff">else</span>:
            <span style="color:#0000ff">raise</span> tree_exceptions.DuplicateKeyError(key=new_node.key)
    new_node.parent = parent
    <span style="color:#008000"><em>#</em></span><span style="color:#008000"><em> If the parent is a Leaf, set the new node to be the root.</em></span>
    <span style="color:#0000ff">if</span> isinstance(parent, Leaf):
        new_node.color = Color.BLACK
        self.root = new_node
    <span style="color:#0000ff">else</span>:
        <span style="color:#0000ff">if</span> new_node.key < parent.key:
            parent.left = new_node
        <span style="color:#0000ff">else</span>:
            parent.right = new_node

        <span style="color:#008000"><em>#</em></span><span style="color:#008000"><em> After the insertion, fix the broken red-black-tree-property.</em></span>
        self._insert_fixup(new_node)</span></span>

与二叉搜索树的不同之处在于,我们使用isinstance来检查节点是普通节点还是叶子节点,而不是检查它是否为None。那是因为我们有叶子节点的叶子类型和常规节点的节点类型。

将新节点插入红黑树后,我们需要修复损坏的红黑树属性。以下小节将讨论使用旋转和着色来修复折断的红黑树。

轮换

旋转操作是为了更改红黑树的结构,并保留其二进制搜索属性。旋转有两种类型:左旋转和右旋转。

左旋

图片6

左旋转将两个节点(

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值