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

 

项目设置

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

 
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

 

什么是红黑树?

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

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

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

一棵典型的红黑树如下图所示。在Python系列中构建森林:红黑树_第1张图片

 

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

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

在Python系列中构建森林:红黑树_第2张图片

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

建造红黑树

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

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

在Python系列中构建森林:红黑树_第3张图片

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

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

在Python系列中构建森林:红黑树_第4张图片

节点

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

在Python系列中构建森林:红黑树_第5张图片

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

Python
 
import enum

class Color(enum.Enum):
    RED = enum.auto()
    BLACK = enum.auto()

为什么要使用枚举?

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

Python
 
@dataclass
class Color:
    RED: str = "red"
    BLACK: str = "black"

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

红黑树节点

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

Python
 
from dataclasses import dataclass

@dataclass
class Node:
    """Red-Black Tree non-leaf node definition."""

    key: Any
    data: Any
    left: Union["Node", Leaf] = Leaf()
    right: Union["Node", Leaf] = Leaf()
    parent: Union["Node", Leaf] = Leaf()
    color: Color = Color.RED

叶节点

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

Python
复制代码
from dataclasses import dataclass

@dataclass
class Leaf:
    color = Color.BLACK

班级概况

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

Python
收缩▲   复制代码
class RBTree:

    def __init__(self) -> None:
        self._NIL: Leaf = Leaf()
        self.root: Union[Node, Leaf] = self._NIL

    def __repr__(self) -> str:
        """Provie the tree representation to visualize its layout."""
        if self.root:
            return (
                f"{type(self)}, root={self.root}, "
                f"tree_height={str(self.get_height(self.root))}"
            )
        return "empty tree"

    def search(self, key: Any) -> Optional[Node]:
        …

    def insert(self, key: Any, data: Any) -> None:
        …

    def delete(self, key: Any) -> None:
        …

    @staticmethod
    def get_leftmost(node: Node) -> Node:
        …

    @staticmethod
    def get_rightmost(node: Node) -> Node:
        …

    @staticmethod
    def get_successor(node: Node) -> Union[Node, Leaf]:
        …

    @staticmethod
    def get_predecessor(node: Node) -> Union[Node, Leaf]:
        …

    @staticmethod
    def get_height(node: Union[Leaf, Node]) -> int:
        …

    def inorder_traverse(self) -> traversal.Pairs:
        …

    def preorder_traverse(self) -> traversal.Pairs:
        …

    def postorder_traverse(self) -> traversal.Pairs:
        …

    def _transplant(
        self, deleting_node: Node, replacing_node: Union[Node, Leaf]
    ) -> None:
        …

    def _left_rotate(self, node_x: Node) -> None:
        …

    def _right_rotate(self, node_x: Node) -> None:
        …

    def _insert_fixup(self, fixing_node: Node) -> None:
        …

    def _delete_fixup(self, fixing_node: Union[Leaf, Node]) -> None:
        …

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

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

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

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

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

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

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

        # After the insertion, fix the broken red-black-tree-property.
        self._insert_fixup(new_node)

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

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

轮换

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

左旋

在Python系列中构建森林:红黑树_第6张图片

左旋转将两个节点(图片中的x和y)从图片的顶部树转移到底部树,并保留其binary-search-tree-properties。

Python
复制代码
def _left_rotate(self, node_x: Node) -> None:
    node_y = node_x.right  # Set node y
    if isinstance(node_y, Leaf):  # Node y cannot be a Leaf
        raise RuntimeError("Invalid left rotate")

    # Turn node y's subtree into node x's subtree
    node_x.right = node_y.left
    if isinstance(node_y.left, Node):
        node_y.left.parent = node_x
    node_y.parent = node_x.parent

    # If node's parent is a Leaf, node y becomes the new root.
    if isinstance(node_x.parent, Leaf):
        self.root = node_y
    # Otherwise, update node x's parent.
    elif node_x == node_x.parent.left:
        node_x.parent.left = node_y
    else:
        node_x.parent.right = node_y

    node_y.left = node_x
    node_x.parent = node_y

请注意,节点x的右子不能为Leaf(即NIL);旋转叶子毫无意义。

右旋

在Python系列中构建森林:红黑树_第7张图片

右旋与左旋对称,可以实现如下。

Python
复制代码
def _right_rotate(self, node_x: Node) -> None:
    node_y = node_x.left  # Set node y
    if isinstance(node_y, Leaf):  # Node y cannot be a Leaf
        raise RuntimeError("Invalid right rotate")
    # Turn node y's subtree into node x's subtree
    node_x.left = node_y.right
    if isinstance(node_y.right, Node):
        node_y.right.parent = node_x
    node_y.parent = node_x.parent

    # If node's parent is a Leaf, node y becomes the new root.
    if isinstance(node_x.parent, Leaf):
        self.root = node_y
    # Otherwise, update node x's parent.
    elif node_x == node_x.parent.right:
        node_x.parent.right = node_y
    else:
        node_x.parent.left = node_y

    node_y.right = node_x
    node_x.parent = node_y

修理

要恢复red-black-tree-property,我们需要知道插入后哪些red-black-property可能被破坏。

红黑树属性:

  1. 每个节点都是红色或黑色(不能折断)。
  2. 根是黑色的。
  3. 每片叶子都是黑色的(因为每个新节点的子节点都指向该叶子,所以不能被破坏
  4. 如果节点为红色,则其两个子节点均为黑色。
  5. 每个节点从节点到叶子的所有路径都包含相同数量的黑色节点(也称为黑色高度)。

对于第5个属性,黑色高度仍然相同。新节点(如红色)替换了LeafNIL),但是它的子节点也是LeafNIL)。因此,黑色高度属性在插入后保持不变。

因此,由于新节点的颜色为红色,因此只有属性2和属性4可能会受到侵犯。如果新节点的父节点也为红色,则属性4损坏。关于属性2,如果在固定属性4时新节点是根或根变成红色,则可能会违反该属性。我们可以通过在固定例程的末尾将其着色为黑色来固定属性2。

关于固定特性4,我们可以将其细分为6种情况(对称地分为3种情况)。这六种情况由新节点的父级的位置和颜色以及父级的同级决定。

 

新节点的父母的位置

新节点的父级兄弟姐妹的颜色

新节点的位置

情况1

左子

红色的

没关系

情况二

左子

黑色的

合适的孩子

情况3

左子

黑色的

左子

案例4

合适的孩子

红色的

没关系

案例5

合适的孩子

黑色的

左子

案例6

合适的孩子

黑色的

合适的孩子

从新节点(fix_node)开始。

情况1

  • fixing_node父级和父级同级的颜色更改为黑色
  • fixing_node的祖父母的颜色更改为红色
  • 将当前位置移动到fixing_node的祖父母

请注意,新节点的位置无关紧要。下图显示了情况1.新节点(7)是左子节点,而2.新节点(18)是右子节点。

在Python系列中构建森林:红黑树_第8张图片

情况二

  • fixing_node的父节点上执行左旋转(此操作将案例2转移到案例3

在Python系列中构建森林:红黑树_第9张图片

情况3

  • fixing_node父级的颜色更改为黑色
  • fixing_node的祖父母的颜色更改为红色
  • fixing_node的祖父母执行正确的旋转

在Python系列中构建森林:红黑树_第10张图片

情况4(与情况1相同)

  • fixing_node父级和父级同级的颜色更改为黑色
  • fixing_node的祖父母的颜色更改为红色
  • 将当前位置移动到fixing_node的祖父母

在Python系列中构建森林:红黑树_第11张图片

案例5

  • fixing_node的父节点上执行右旋转(此操作将案例5转移到案例6

在Python系列中构建森林:红黑树_第12张图片

案例6

  • fixing_node父级的颜色更改为黑色
  • fixing_node的祖父母的颜色更改为红色

在Python系列中构建森林:红黑树_第13张图片

在为fixing_node修复损坏的红黑树属性时,修复过程可能会导致fixing_node的父级或祖父母(视情况而定)违反红黑树属性。发生这种情况时,fixing_node的父(或祖父母)将成为新的fixing_node。然后我们可以根据这六个案例解决它。重复此步骤,直到到达根目录为止。如果在修复操作之后根变成红色(违反了属性2),我们可以通过将根着色为黑色来修复它。

下图演示了构建红黑树的完整插入操作。

在Python系列中构建森林:红黑树_第14张图片

插入固定的实现如下所示。

Python
收缩▲   复制代码
def _insert_fixup(self, fixing_node: Node) -> None:
    while fixing_node.parent.color == Color.RED:
        if fixing_node.parent == fixing_node.parent.parent.left:
            parent_sibling = fixing_node.parent.parent.right
            if parent_sibling.color == Color.RED:  # Case 1
                fixing_node.parent.color = Color.BLACK
                parent_sibling.color = Color.BLACK
                fixing_node.parent.parent.color = Color.RED
                fixing_node = fixing_node.parent.parent
            else:
                # Case 2
                if fixing_node == fixing_node.parent.right:
                    fixing_node = fixing_node.parent
                    self._left_rotate(fixing_node)
                # Case 3
                fixing_node.parent.color = Color.BLACK
                fixing_node.parent.parent.color = Color.RED
                self._right_rotate(fixing_node.parent.parent)
        else:
            parent_sibling = fixing_node.parent.parent.left
            if parent_sibling.color == Color.RED:  # Case 4
                fixing_node.parent.color = Color.BLACK
                parent_sibling.color = Color.BLACK
                fixing_node.parent.parent.color = Color.RED
                fixing_node = fixing_node.parent.parent
            else:
                # Case 5
                if fixing_node == fixing_node.parent.left:
                    fixing_node = fixing_node.parent
                    self._right_rotate(fixing_node)
                # Case 6
                fixing_node.parent.color = Color.BLACK
                fixing_node.parent.parent.color = Color.RED
                self._left_rotate(fixing_node.parent.parent)

    self.root.color = Color.BLACK

搜索

搜索操作类似于二进制搜索树,但是我们使用LeafNIL)确定是否到达叶子。

  1. 从根开始遍历树,并在树遍历中将密钥与每个节点的密钥进行比较。
  2. 如果键匹配,我们找到了该节点。
  3. 如果到达Leaf,则该节点在树中不存在。

 该搜索的实现方式类似于二叉搜索树搜索功能。

Python
复制代码
def search(self, key: Any) -> Optional[Node]:
    current = self.root

    while isinstance(current, Node):
        if key < current.key:
            current = current.left
        elif key > current.key:
            current = current.right
        else:  # Key found
            return current
    # If the tree is empty (i.e., self.root == Leaf()), still return None.
    return None

删除

与插入红黑树相似,删除操作会修改红黑树,结果可能会违反红黑树属性。因此,我们需要在删除节点后恢复red-black-tree-property。

红黑树删除的基本思想类似于普通的二进制搜索树。我们有一个移植的方法,以取代在根的子树deleting_node与在根的子树replacing_node。我们还有三种基本的删除案例:无孩子,只有一个孩子和两个孩子。最后,我们需要修复损坏的红黑树属性。

移植

移植方法是从修改后的二叉搜索树,所以它的工作原理适当的红黑树。修改之处在于,我们使用isinstance来检查节点是普通节点还是Leaf节点,而不是检查它是否为None

Python
复制代码
def _transplant(
    self, deleting_node: Node, replacing_node: Union[Node, Leaf]
) -> None:
    if isinstance(deleting_node.parent, Leaf):
        self.root = replacing_node
    elif deleting_node == deleting_node.parent.left:
        deleting_node.parent.left = replacing_node
    else:
        deleting_node.parent.right = replacing_node

    if isinstance(replacing_node, Node):
        replacing_node.parent = deleting_node.parent

整个删除过程就像经过一些修改的普通二进制搜索树。

  1. 查找要删除的节点(delete_node)。
  2. 保持颜色deleting_node
  3. 如果deleting_node没有或只有一个孩子,用移植的方法来代替deleting_node无论使用哪种NIL或只生一个孩子。
  4. 如果deleteing_node有两个子级,请找到Deleteing_node的后继者为replaceing_node。保持颜色replacing_node。使用方法移植取出replacing_node,并保持跟踪的节点更换replacing_node,无论是NILreplacing_node的原始右孩子。使用移植方法来代替deleting_nodereplacing_node。将replaceing_node的颜色设置为deleteing_node的颜色。
  5. 通过更改颜色并执行旋转来修复损坏的红黑树属性。

基于上面的删除过程,我们可以实现以下删除方法。

Python
收缩▲   复制代码
def delete(self, key: Any) -> None:
    if (deleting_node := self.search(key=key)) and (
        not isinstance(deleting_node, Leaf)
    ):
        original_color = deleting_node.color

        # Case 1: no children or Case 2a: only one right child
        if isinstance(deleting_node.left, Leaf):
            replacing_node = deleting_node.right
            self._transplant(
                deleting_node=deleting_node, replacing_node=replacing_node
            )
            # Fixup
            if original_color == Color.BLACK:
                if isinstance(replacing_node, Node):
                    self._delete_fixup(fixing_node=replacing_node)

        # Case 2b: only one left child
        elif isinstance(deleting_node.right, Leaf):
            replacing_node = deleting_node.left
            self._transplant(
                deleting_node=deleting_node, replacing_node=replacing_node
            )
            # Fixup
            if original_color == Color.BLACK:
                self._delete_fixup(fixing_node=replacing_node)

        # Case 3: two children
        else:
            replacing_node = self.get_leftmost(deleting_node.right)
            original_color = replacing_node.color
            replacing_replacement = replacing_node.right
            # The replacing node is not the direct child of the deleting node
            if replacing_node.parent != deleting_node:
                self._transplant(replacing_node, replacing_node.right)
                replacing_node.right = deleting_node.right
                replacing_node.right.parent = replacing_node

            self._transplant(deleting_node, replacing_node)
            replacing_node.left = deleting_node.left
            replacing_node.left.parent = replacing_node
            replacing_node.color = deleting_node.color
            # Fixup
            if original_color == Color.BLACK:
                if isinstance(replacing_replacement, Node):
                    self._delete_fixup(fixing_node=replacing_replacement)

值得一提的是:

  • 我们保持原来的颜色(original_color中的)deleting_node
  • 如果要删除的节点有两个子节点,我们将original_color更新为该节点的颜色(replace_node)。
  • 在每种情况下,如果original_color为黑色,则意味着某些red-black-tree-property已损坏,我们需要对其进行修复。

在还原违规的red-black-tree属性之前,我们需要知道在删除例程期间哪些red-black-property可能会被破坏。

红黑树属性:

  1. 每个节点都是红色或黑色
  2. 根是黑色的
  3. 每个叶子都是黑色的(因为每个新节点的子节点都指向Leaf,所以不能被破坏)。
  4. 如果节点为红色,则其两个子节点均为黑色
  5. 每个节点从节点到叶子的所有路径都包含相同数量的黑色节点(也称为黑色高度)

情况1:要删除的节点没有子节点

在Python系列中构建森林:红黑树_第15张图片

如果颜色deleting_node赤,人无红黑树属性被打破。如果delete_node的颜色为黑色,则违反属性5。

情况2:要删除的节点只有一个孩子

由于具有红黑树属性,一个红色节点不可能只有一个黑人孩子。一个黑色节点也不可能只有一个黑色子节点。因此,如果deleting_node只有一个孩子,它的颜色必须是黑色,而颜色replacing_node必须为红色。

在Python系列中构建森林:红黑树_第16张图片

根据上面的图片,如果deleteing_node是黑色,则属性4和属性5以及属性2都可以被破坏(如果deleteingnode是根)。

情况3:要删除的节点有两个子节点

在这种情况下,我们执行以下步骤:

  1. 找到最左边的节点(replacing_node)为节点,以取代deleting_node从的右子树deleting_node
  2. 通过执行移植操作,将replaceing_node移出。
  3. 设置replacing_node作为右子树的新根deleting_node
  4. 执行移植手术更换deleting_node与根的replacing_node的子树。
  5. replacement_node的颜色设置为deleteing_node的颜色

第5步之后,颜色replacing_node是一样的deleting_node,所以没有红黑树属性被打破。这可能会破坏红黑树属性的唯一一步是一步2.当我们进行移植的操作replacing_node,它最终被这两种情况下1或2的情况下。

下图演示了删除具有两个子节点的节点可能会或可能不会违反red-black-tree-property。

该案件replacing_node是直接孩子deleting_node

在Python系列中构建森林:红黑树_第17张图片

该案件replacing_node是黑色的,而不是直接孩子deleting_node

在Python系列中构建森林:红黑树_第18张图片

该案件replacing_node是红色的,而不是直接孩子deleting_node

在Python系列中构建森林:红黑树_第19张图片

因此,我们可以总结需要修复损坏的红黑树属性的情况。

没有孩子

如果要删除的节点为黑色

只有一个孩子

如果要删除的节点为黑色

两个孩子

如果要替换的节点(从要删除的节点的右子树的最左端)是黑色的

该摘要还暗示,在以下情况下,红黑树属性仍然成立。

  1. 删除节点为红色,并且子节点少于两个。
  2. 删除节点有两个子节点,替换节点为红色。

原因如下:

  • 黑色高度没有变化。物业5成立。
  • 对于第一种情况,如果要删除的节点是根节点,则不能为红色;如果是根节点,则不能为红色。对于第二种情况,最左边的节点不能是根。物业2成立。
  • 如果节点(第一种或第二种情况)为红色,则其父代和子代不能为红色。因此,将其删除或移动后,连续的红色节点将不会发生。物业4成立。

修理

要修复损坏的红黑树属性,我们使用算法导论中的思想,并且修复过程首先通过引入该概念来修复属性5(从节点到叶子的每个节点的黑色高度都相同)。黑色和红色和黑色。对于黑色高度,双黑和红黑分别贡献2或1。并且,我们使用以下图标表示黑色和红色黑色节点。

图片20

当我们使用移植功能将一个节点替换为另一个节点时,我们保留两个节点的颜色,而不是替换该节点,并使用双黑和红黑两种颜色表示其颜色。因此,如果要删除的节点少于两个子节点,则在移植功能之后,替换节点的颜色将变为双黑或红黑。如果要删除的节点有两个子节点,则当我们使用移植功能取出要删除的节点所扎根的子树的最左边的节点时,最左边的节点替换的颜色将变为黑色或红色和黑色。。为了简单起见,我们使用fixing_node指示节点的颜色为双黑或红黑。请注意,在代码实现中,我们并没有真正将fixing_node着色为双黑或红黑。我们只是假装fixing_node有一个额外的黑色或红色。

这样,可以解决无效的黑色高度,并且可能出现的破损情况如下:

要删除的节点没有子节点

在Python系列中构建森林:红黑树_第20张图片

要删除的节点只有一个孩子

在Python系列中构建森林:红黑树_第21张图片

要删除的节点有两个子节点

如果要删除的节点有两个子节点,则位于最左侧节点位置的节点将破坏红黑树属性。

replacing_node是直接孩子deleting_node

在Python系列中构建森林:红黑树_第22张图片

replacing_node不是直接孩子deleting_node

在Python系列中构建森林:红黑树_第23张图片

由于fixing_node的颜色既不是红色也不是黑色,因此属性1也被破坏了。现在,我们需要还原red-black-tree-property 1、2和4。

如果fixing_node的颜色是红色和黑色,我们可以通过将其着色为黑色来对其进行修复。恢复所有损坏的红黑树属性。

在Python系列中构建森林:红黑树_第24张图片

 

在Python系列中构建森林:红黑树_第25张图片

剩下的残破情况是双黑的。修复程序可以分为四个对称情况。

在Python系列中构建森林:红黑树_第26张图片

该案件是由位置确定fixing_node的颜色fixing_node的兄弟姐妹,以及兄弟姐妹的子女的颜色。

 

fixing_node

兄弟姐妹的颜色

兄弟姐妹左孩子的颜色

兄弟姊妹的孩子的颜色

情况1

左子

红色的

没关系

没关系

情况二

左子

黑色的

黑色的

黑色的

情况3

左子

黑色的

红色的

黑色的

案例4

左子

黑色的

黑色的

红色的

案例5

合适的孩子

红色的

没关系

没关系

案例6

合适的孩子

黑色的

黑色的

黑色的

案例7

合适的孩子

黑色的

黑色的

红色的

案例8

合适的孩子

黑色的

红色的

黑色的

fixing_node开始。

情况1

  • 将兄弟节点的颜色更改为黑色
  • fixing_node父级的颜色更改为红色
  • fixing_node的父节点上执行左旋转
  • 更新同级节点(同级因左旋转而更改)
  • 完成上述操作后,案例1转移到案例2,案例3或案例4

在Python系列中构建森林:红黑树_第27张图片

情况二

  • 将同胞的颜色更改为红色
  • 向上移动fixing_node,即新的fixing_node成为原始fixing_node的父级

在Python系列中构建森林:红黑树_第28张图片

情况3

  • 将同胞的左孩子的颜色更改为黑色
  • 将同胞的颜色更改为红色
  • 在同级节点上执行右旋转
  • 完成上述操作后,情况3转移到情况4

在Python系列中构建森林:红黑树_第29张图片

案例4

  • 将兄弟姐妹的颜色更改为与fixing_node的父对象相同的颜色
  • fixing_node父级的颜色更改为黑色
  • 将同级节点的右子级的颜色更改为黑色
  • fixing_nopde的父对象上执行左旋转
  • 完成上述操作后,所有违规的红黑树属性均已恢复。

在Python系列中构建森林:红黑树_第30张图片

案例5

  • 将兄弟节点的颜色更改为黑色
  • fixing_node父级的颜色更改为红色
  • fixing_node的父节点上执行正确的旋转
  • 更新同级节点(同级由于右旋转而更改)
  • 完成上述操作后,案例1转移到案例6,案例7或案例8

在Python系列中构建森林:红黑树_第31张图片

案例6

  • 将同胞的颜色更改为红色
  • 向上移动fixing_node,即新的fixing_node成为原始fixing_node的父级

在Python系列中构建森林:红黑树_第32张图片

案例7

  • 将同胞的合适子女的颜色更改为黑色
  • 将同胞的颜色更改为红色
  • 在同级节点上执行左旋转
  • 完成上述操作后,案例7转移到案例8

在Python系列中构建森林:红黑树_第33张图片

案例8

  • 将兄弟姐妹的颜色更改为与fixing_node的父对象相同的颜色
  • fixing_node父级的颜色更改为黑色
  • 将兄弟节点左孩子的颜色更改为黑色
  • fixing_nopde的父项执行正确的旋转
  • 完成上述操作后,所有违规的红黑树属性均已恢复。

在Python系列中构建森林:红黑树_第34张图片

以下实现总结了上面讨论的修复过程。

Python
收缩▲   复制代码
def _delete_fixup(self, fixing_node: Union[Leaf, Node]) -> None:
    while (fixing_node is not self.root) and (fixing_node.color == Color.BLACK):
        if fixing_node == fixing_node.parent.left:
            sibling = fixing_node.parent.right

            # Case 1: the sibling is red.
            if sibling.color == Color.RED:
                sibling.color == Color.BLACK
                fixing_node.parent.color = Color.RED
                self._left_rotate(fixing_node.parent)
                sibling = fixing_node.parent.right

            # Case 2: the sibling is black and its children are black.
            if (sibling.left.color == Color.BLACK) and (
                sibling.right.color == Color.BLACK
            ):
                sibling.color = Color.RED
                fixing_node = fixing_node.parent  # new fixing node

            # Cases 3 and 4: the sibling is black and one of
            # its child is red and the other is black.
            else:
                # Case 3: the sibling is black and its left child is red.
                if sibling.right.color == Color.BLACK:
                    sibling.left.color = Color.BLACK
                    sibling.color = Color.RED
                    self._right_rotate(node_x=sibling)

                # Case 4: the sibling is black and its right child is red.
                sibling.color = fixing_node.parent.color
                fixing_node.parent.color = Color.BLACK
                sibling.right.color = Color.BLACK
                self._left_rotate(node_x=fixing_node.parent)
                # Once we are here, all the violation has been fixed, so
                # move to the root to terminate the loop.
                fixing_node = self.root
        else:
            sibling = fixing_node.parent.left

            # Case 5: the sibling is red.
            if sibling.color == Color.RED:
                sibling.color == Color.BLACK
                fixing_node.parent.color = Color.RED
                self._right_rotate(node_x=fixing_node.parent)
                sibling = fixing_node.parent.left

            # Case 6: the sibling is black and its children are black.
            if (sibling.right.color == Color.BLACK) and (
                sibling.left.color == Color.BLACK
            ):
                sibling.color = Color.RED
                fixing_node = fixing_node.parent
            else:
                # Case 7: the sibling is black and its right child is red.
                if sibling.left.color == Color.BLACK:
                    sibling.right.color = Color.BLACK
                    sibling.color = Color.RED
                    self._left_rotate(node_x=sibling)
                # Case 8: the sibling is black and its left child is red.
                sibling.color = fixing_node.parent.color
                fixing_node.parent.color = Color.BLACK
                sibling.left.color = Color.BLACK
                self._right_rotate(node_x=fixing_node.parent)
                # Once we are here, all the violation has been fixed, so
                # move to the root to terminate the loop.
                fixing_node = self.root

    fixing_node.color = Color.BLACK

辅助功能

除了核心功能(即插入,搜索和删除)之外,红黑树还可以具有其他有用的功能,例如获取最左边的节点,获取节点的后继者以及获取树的高度,其实现类似于二叉搜索树辅助功能,但有一些修改。

取得身高

要计算一棵红黑树的树高,我们可以像在二叉搜索树中一样,对每个孩子的身高递归地将其高度加一。如果一个节点有两个子节点,则使用max函数从这些子节点获取更大的高度,并将最高的子节点增加一个。主要区别在于我们使用isinstance检查节点是否具有叶子。

Python
复制代码
@staticmethod
def get_height(node: Union[Leaf, Node]) -> int:
    if isinstance(node, Node):
        if isinstance(node.left, Node) and isinstance(node.right, Node):
            return (
                max(RBTree.get_height(node.left), RBTree.get_height(node.right)) + 1
            )

        if isinstance(node.left, Node):
            return RBTree.get_height(node=node.left) + 1

        if isinstance(node.right, Node):
            return RBTree.get_height(node=node.right) + 1

    return 0

获取最左边和最右边的节点

红黑树与二叉搜索树的功能相同,它可以获取给定(子)树的最右边节点和给定(子)树的最左边节点。再次,我们使用isinstance检查节点是常规的红黑树节点还是叶子。

 

最左边

Python
复制代码
@staticmethod
def get_leftmost(node: Node) -> Node:
    current_node = node
    while isinstance(current_node.left, Node):
        current_node = current_node.left
    return current_node

最右边

Python
复制代码
@staticmethod
def get_rightmost(node: Node) -> Node:
    current_node = node
    while isinstance(current_node.right, Node):
        current_node = current_node.right
    return current_node

前任和后继

红黑树与二进制搜索树的功能相同,它可以获取节点的前任和后继。

前任

Python
复制代码
@staticmethod
def get_predecessor(node: Node) -> Union[Node, Leaf]:
    if isinstance(node.left, Node):
        return RBTree.get_rightmost(node=node.left)
    parent = node.parent
    while isinstance(parent, Node) and node == parent.left:
        node = parent
        parent = parent.parent
    return node.parent

接班人

Python
复制代码
@staticmethod
def get_successor(node: Node) -> Union[Node, Leaf]:
    if isinstance(node.right, Node):
        return RBTree.get_leftmost(node=node.right)
    parent = node.parent
    while isinstance(parent, Node) and node == parent.right:
        node = parent
        parent = parent.parent
    return parent

遍历

由于叶节点的原因,我们无法使用在二叉树遍历中实现的遍历函数。但是,遍历的概念是相同的。我们只需要简单的修改即可:使用isinstance检查节点是否为常规的红黑树节点或叶子。

有序遍历

Python
复制代码
def inorder_traverse(self) -> traversal.Pairs:
    return self._inorder_traverse(node=self.root)

def _inorder_traverse(self, node: Union[Node, Leaf]) -> traversal.Pairs:
    if isinstance(node, Node):
        yield from self._inorder_traverse(node.left)
        yield (node.key, node.data)
        yield from self._inorder_traverse(node.right)

预购遍历

Python
复制代码
def preorder_traverse(self) -> traversal.Pairs:
    return self._preorder_traverse(node=self.root)

def _preorder_traverse(self, node: Union[Node, Leaf]) -> traversal.Pairs:
    if isinstance(node, Node):
        yield (node.key, node.data)
        yield from self._preorder_traverse(node.left)
        yield from self._preorder_traverse(node.right)

后遍历

Python
复制代码
def postorder_traverse(self) -> traversal.Pairs:
    return self._postorder_traverse(node=self.root)

def _postorder_traverse(self, node: Union[Node, Leaf]) -> traversal.Pairs:
    if isinstance(node, Node):
        yield from self._postorder_traverse(node.left)
        yield from self._postorder_traverse(node.right)
        yield (node.key, node.data)

注意,返回类型(traversal.Pairs)在定义traversal.py从二叉树的遍历。

测试

与往常一样,我们应该对我们的代码进行尽可能多的单元测试。在这里,我们使用basic_tree功能conftest.py我们在创建构建二叉搜索树来测试我们的红黑树。检查test_red_black_tree.py以进行完整的单元测试。

分析

正如我们在“二分搜索树:分析”中讨论的那样,我们知道二分搜索树的操作运行时基于树的高度。红黑树是一种自平衡二叉搜索树,高度最大为2 * lg(n + 1)= O(lg n),其中n是节点数。(证明可参考算法引理13.1)。因此,可以在下表中总结一棵红黑树的时间复杂度。

在Python系列中构建森林:红黑树_第35张图片

例子

由于具有自平衡功能,红黑树在软件程序中广泛使用,包括实现其他数据结构。例如,C ++ STL映射被实现为一棵红黑树。本节使用我们在此处实现的红黑树来实现键值Map

Python
收缩▲   复制代码
from typing import Any, Optional

from forest.binary_trees import red_black_tree
from forest.binary_trees import traversal

class Map:
    """Key-value Map implemented using Red-Black Tree."""

    def __init__(self) -> None:
        self._rbt = red_black_tree.RBTree()

    def __setitem__(self, key: Any, value: Any) -> None:
        """Insert (key, value) item into the map."""
        self._rbt.insert(key=key, data=value)

    def __getitem__(self, key: Any) -> Optional[Any]:
        """Get the data by the given key."""
        node = self._rbt.search(key=key)
        if node:
            return node.data
        return None

    def __delitem__(self, key: Any) -> None:
        """Remove a (key, value) pair from the map."""
        self._rbt.delete(key=key)

    def __iter__(self) -> traversal.Pairs:
        """Iterate the data in the map."""
        return self._rbt.inorder_traverse()

if __name__ == "__main__":

    # Initialize the Map instance.
    contacts = Map()

    # Add some items.
    contacts["Mark"] = "[email protected]"
    contacts["John"] = "[email protected]"
    contacts["Luke"] = "[email protected]"

    # Retrieve an email
    print(contacts["Mark"])

    # Delete one item.
    del contacts["John"]

    # Check the deleted item.
    print(contacts["John"])  # This will print None

    # Iterate the items.
    for contact in contacts:
        print(contact)

完整的示例如下

https://github.com/shunsvineyard/forest-python/blob/main/examples/rbt_map.py

 

你可能感兴趣的:(python,数据结构,算法)