65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Python 中构建森林系列:单向二叉搜索树

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (3投票s)

2021年4月3日

CPOL

16分钟阅读

viewsIcon

12532

downloadIcon

108

使用 Python 构建线程二叉搜索树。

引言

本文是“Python 系列:构建森林”系列的第三篇文章。在上篇文章《Python 系列:二叉树遍历》中,我们讨论了使用递归和辅助栈来实现二叉树的遍历。本文将构建二叉搜索树的一个变种——线索二叉搜索树。线索二叉搜索树利用空指针(左或右子节点为空)的属性来实现某些特定顺序的遍历,而无需使用栈或递归。

项目设置

与“Python 系列:构建二叉搜索树”系列中的其他文章一样,本文的实现假定使用 Python 3.9 或更高版本。本文为我们的项目增加了两个模块:用于线索二叉搜索树实现的 `single_threaded_binary_trees.py` 和用于单元测试的 `test_single_threaded_binary_trees.py`。添加这两个文件后,我们的项目结构如下:

forest-python
├── forest
│   ├── __init__.py
│   ├── binary_trees
│   │   ├── __init__.py
│   │   ├── binary_search_tree.py
│   │   ├── single_threaded_binary_trees.py
│   │   └── traversal.py
│   └── tree_exceptions.py
└── tests
    ├── __init__.py
    ├── conftest.py
    ├── test_binary_search_tree.py
    ├── test_single_threaded_binary_trees.py
    └── test_traversal.py

(完整代码可在 forest-python 获取。)

什么是线索二叉树?

线索二叉树是二叉树的一种变种,它通过利用节点的空左指针或右指针来优化特定顺序的遍历。线索二叉树有两种类型:

  • 单线索二叉树 (Single-threaded binary tree):对于节点中空的左指针或右指针,它会被“线索化”,指向该节点的中序前驱或后继。换句话说,左指针或右指针不再为空,而是指向节点的(前驱或后继)。单线索二叉树有两种实现方式:左单线索二叉树和右单线索二叉树。
    • 右单线索二叉树 (Right single-threaded binary tree):节点的空右指针指向该节点的后继,如果节点有空的左指针,则该指针保持为空。
    • 左单线索二叉树 (Left single-threaded binary tree):节点的空左指针指向该节点的前驱,如果节点有空的右指针,则该指针保持为空。
  • 双线索二叉树 (Double-threaded binary tree):对于任何空的左指针或右指针,这些空指针都会被线索化,指向中序前驱或后继:如果左指针为空,则指向前驱;如果右指针为空,则指向后继。

虽然线索可以添加到任何二叉树中,但最适合将其添加到二叉搜索树或其变种(即满足二叉搜索树性质的树)中。因此,在本项目中,我们将实现的线索二叉树是带有线索的二叉搜索树(即线索二叉搜索树)。在文章的其余部分,为了避免术语冗余,我们提及的线索二叉树都将特指二叉搜索树。

此外,线索不一定需要指向中序前驱或后继,也可以指向前序或后序。但是,中序是二叉搜索树的有序序列,所以指向中序前驱或后继的线索是最常见的。

我们为什么需要线索?

为二叉树添加线索会增加复杂性,那么我们为什么需要线索呢?有几个原因:

  • 快速访问后继或前驱
  • 某些遍历方式无需辅助栈或递归
  • 由于无需辅助栈或递归,减少了执行遍历时的内存消耗
  • 利用了被浪费的空间。由于节点中空的左指针或右指针不存储任何信息,我们可以将其用作线索。

对于每种类型的线索二叉树,我们可以总结添加线索的好处如下:

  • 右单线索二叉树无需栈或递归即可执行中序和前序遍历。它还提供了快速访问后继的功能。
  • 左单线索二叉树无需栈或递归即可执行反中序遍历,并提供快速访问前驱的功能。
  • 双线索二叉树兼具两种单线索二叉树的优点。

构建单线索二叉搜索树

正如我们在“构建二叉搜索树”部分所做的那样,本节将逐步介绍实现过程,并讨论一些实现选择背后的想法。

节点

节点结构与二叉搜索树节点类似,但增加了一个额外的字段 `is_thread`。`is_thread` 属性的目的是区分左指针或右指针是普通指针还是线索。

`is_thread` 属性是一个布尔变量:如果该指针(根据单线索二叉树的类型,为左指针或右指针)是线索,则为 `True`;否则为 `False`。

@dataclasses.dataclass
class Node:
    key: Any
    data: Any
    left: Optional["Node"] = None
    right: Optional["Node"] = None
    parent: Optional["Node"] = None
    is_thread: bool = False

与二叉搜索树节点一样,我们将线索二叉树的节点类定义为数据类 (dataclass)

右单线索二叉搜索树

顾名思义,我们可以通过下图来形象地理解右单线索二叉树:

每个节点的空右指针都指向其中序后继,并且其 `is_thread` 变量设置为 `True`,除了最右边的节点。最右边节点的右指针仍然为空,其 `is_thread` 为 `False`,这样我们就知道它是最右边的节点(即没有后继了)。

与二叉搜索树一样,右单线索二叉树也拥有核心功能(`insert`、`delete` 和 `search`)用于构建和修改,以及其他不与特定树绑定的辅助功能,例如获取最左节点和获取树的高度。我们在二叉搜索树中实现的 `__repr__()` 函数也可以用于调试目的。

这里的主要区别在于,我们实现了无需栈或递归即可进行中序和前序遍历的方法。

class RightThreadedBinaryTree:

    def __init__(self) -> None:
        self.root: Optional[Node] = None

    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) -> Optional[Node]:
        …

    @staticmethod
    def get_predecessor(node: Node) -> Optional[Node]:
        …

    @staticmethod
    def get_height(node: Optional[Node]) -> int:
        …

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

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

Insert

`insert` 操作类似于二叉搜索树的插入。区别在于线索二叉树需要考虑线索的更新。因此,插入步骤如下:

  1. 通过从根节点开始遍历树,并将新节点键与沿途每个节点的键进行比较,找到插入新节点的合适位置(即新节点的父节点)。向右子树遍历时,还要检查 `is_thread` 变量。如果该变量为 `True`,则表示我们已到达叶子节点,该叶子节点就是新节点的父节点。
  2. 更新新节点的 parent 属性以指向父节点。
  3. 如果新节点是父节点的左子节点,则将新节点的右指针指向父节点,并将 `is_thread` 变量设置为 `True`。更新父节点的左指针指向新节点。
  4. 如果新节点是其父节点的右子节点,则将父节点的右指针(在插入之前,父节点的右指针必须是线索)复制到新节点的右指针,并将 `is_thread` 变量设置为 `True`。更新父节点的右指针指向新节点,并将父节点的 `is_thread` 设置为 `False`。

下图演示了节点插入的步骤。

我们可以按以下方式实现插入:

def insert(self, key: Any, data: Any) -> None:
    new_node = Node(key=key, data=data)
    parent: Optional[Node] = None
    current: Optional[Node] = self.root

    while current:
        parent = current
        if new_node.key < current.key:
            current = current.left
        elif new_node.key > current.key:
            # If the node is thread, meaning it's a leaf node.
            if current.is_thread:
                current = None
            else:
                current = current.right
        else:
            raise tree_exceptions.DuplicateKeyError(key=new_node.key)
    new_node.parent = parent
    # If the tree is empty
    if parent is None:
        self.root = new_node
    elif new_node.key < parent.key:
        parent.left = new_node

        # Update thread
        new_node.right = parent
        new_node.is_thread = True

    else:
        # Update thread
        new_node.is_thread = parent.is_thread
        new_node.right = parent.right
        parent.is_thread = False
        # Parent's right must be set after thread update
        parent.right = new_node

搜索

搜索操作也类似于二叉搜索树的搜索,但需要检查 `is_thread` 变量来确定我们是否到达了叶子节点。

  1. 从根节点开始遍历树,并将要查找的键与沿途每个节点的键进行比较。
  2. 如果键匹配,则找到了该节点。
  3. 如果在到达叶子节点后(如果 `is_thread` 为 `True`,则表示该节点是叶子节点)仍未找到匹配的键,则表示该键不存在于树中。

实现与我们在二叉搜索树中进行的实现相似,只需进行一个简单的修改——检查 `is_thread`。

def search(self, key: Any) -> Optional[Node]:
    current = self.root
    while current:
        if key == current.key:
            return current
        elif key < current.key:
            current = current.left
        else:  # key > current.key
            if current.is_thread:
                break
            current = current.right
    return None

删除

与二叉搜索树一样,要删除的右单线索二叉树节点有三种情况:无子节点、只有一个子节点、有两个子节点。我们还使用在“Python 系列:二叉搜索树 - 删除”中使用的替换(transplant)技术来用要删除的节点替换子树。虽然基本思想相同,但 `transplant` 函数和 `delete` 函数都需要考虑右线索。我们最需要记住的是,在删除节点时,如果其他节点的右指针指向要删除的节点,我们需要更新该节点的线索(即右指针)。

情况 1:无子节点

如果待删除节点没有子节点,则其左指针为空,`is_thread` 为 `True`。关于线索,我们需要考虑两种情况。请看下图:

情况 2a:只有一个左子节点

如果待删除节点只有一个左子节点,这意味着节点的 `is_thread` 为 `True`。(例外情况是当待删除节点是根节点且只有一个左子节点时;在这种情况下,节点的 `is_thread` 为 `False` 且其右指针为 `None`)。需要更新线索的情况如下图所示:

情况 2b:只有一个右子节点

如果待删除节点只有一个右子节点,这意味着节点的左指针为空,`is_thread` 为 `False`。由于待删除节点没有左子节点,这意味着没有人指向该节点。

情况 3:有两个子节点

与二叉搜索树的删除类似,待删除节点有两个子节点的情况可以分解为两种子情况:

3.a 要删除节点的右子节点也是右子树中最左边的节点。

在这种情况下,右子节点必须只有一个右子节点。因此,我们可以用右子节点替换被删除的节点,如下图所示:

3.b 要删除节点的右子节点也有两个子节点。

在这种情况下,我们从右子树中找到最左边的节点来替换被删除的节点。请注意,当我们从右子树中移除最左边的节点时,它也属于删除情况:情况 1(无子节点)或情况 2(只有一个右子节点)。否则,它就不可能是最左边的节点。

因此,我们两次使用 `transplant` 函数:一次移除最左边的节点,另一次用原来的最左边的节点替换被删除的节点。下图演示了在执行删除时需要考虑线索的情况。

被替换节点没有子节点

被替换节点只有一个右子节点:

根据上图,我们可以实现 `delete` 和 `transplant` 函数如下:

def delete(self, key: Any) -> None:
    if self.root and (deleting_node := self.search(key=key)):
        # Case 1: no child
        if deleting_node.left is None and (
            deleting_node.right is None or deleting_node.is_thread
        ):
            self._transplant(deleting_node=deleting_node, replacing_node=None)

        # Case 2a: only one left child
        elif deleting_node.left and (
            deleting_node.is_thread or deleting_node.right is None
            # deleting_node.right is None means the deleting node is the root.
        ):
            predecessor = self.get_predecessor(node=deleting_node)
            if predecessor:
                predecessor.right = deleting_node.right
            self._transplant(
                deleting_node=deleting_node, replacing_node=deleting_node.left
            )

        # Case 2b: only one right child
        elif deleting_node.left is None and deleting_node.is_thread is False:
            self._transplant(
                deleting_node=deleting_node, replacing_node=deleting_node.right
            )

        # Case 3: two children
        elif (
            deleting_node.left
            and deleting_node.right
            and deleting_node.is_thread is False
        ):
            predecessor = self.get_predecessor(node=deleting_node)
            replacing_node: Node = self.get_leftmost(node=deleting_node.right)
            # the leftmost node is not the direct child of the deleting node
            if replacing_node.parent != deleting_node:
                if replacing_node.is_thread:
                    self._transplant(
                        deleting_node=replacing_node, replacing_node=None
                    )
                else:
                    self._transplant(
                        deleting_node=replacing_node,
                        replacing_node=replacing_node.right,
                   )
                replacing_node.right = deleting_node.right
                replacing_node.right.parent = replacing_node
                replacing_node.is_thread = False

            self._transplant(
                deleting_node=deleting_node, replacing_node=replacing_node
            )
            replacing_node.left = deleting_node.left
            replacing_node.left.parent = replacing_node
            if predecessor and predecessor.is_thread:
                predecessor.right = replacing_node
        else:
            raise RuntimeError("Invalid case. Should never happened")

def _transplant(self, deleting_node: Node, replacing_node: Optional[Node]) -> None:
    if deleting_node.parent is None:
        self.root = replacing_node
        if self.root:
            self.root.is_thread = False
    elif deleting_node == deleting_node.parent.left:
        deleting_node.parent.left = replacing_node
        if replacing_node:
            if deleting_node.is_thread:
                if replacing_node.is_thread:
                    replacing_node.right = replacing_node.right
    else:  # deleting_node == deleting_node.parent.right
        deleting_node.parent.right = replacing_node
        if replacing_node:
            if deleting_node.is_thread:
                if replacing_node.is_thread:
                    replacing_node.right = replacing_node.right
        else:
            deleting_node.parent.right = deleting_node.right
            deleting_node.parent.is_thread = True

    if replacing_node:
        replacing_node.parent = deleting_node.parent

获取高度

要计算线索二叉树的高度,我们可以像在二叉搜索树中那样,递归地为每个子节点的高度加一。如果一个节点有两个子节点,我们使用 max 函数获取子节点中较高的那个高度,然后加一。主要区别在于我们使用 `is_thread` 来检查一个节点是否有右子节点。

获取最左侧和最右侧节点

获取最左节点和最右节点的实现与二叉搜索树的实现类似。要获取最右节点,除了检查右指针是否为空外,我们还需要检查 `is_thread` 是否为 `True`。因此,我们可以修改 `get_rightmost` 函数如下:

@staticmethod
def get_rightmost(node: Node) -> Node:
    current_node = node
     while current_node.is_thread is False and current_node.right:
        current_node = current_node.right
    return current_node

`get_leftmost` 的实现与二叉搜索树中的 `get_leftmost` 完全相同。

@staticmethod
def get_leftmost(node: Node) -> Node:
    current_node = node
    while current_node.left:
        current_node = current_node.left
    return current_node

前驱和后继

由于右单线索二叉树提供快速访问中序后继(如果节点的右指针是线索,则指向其后继),我们可以通过跟随其右指针来获取节点的后继,如果右指针是线索。否则,该节点的后继是其右子树中最左边的节点。

@staticmethod
def get_successor(node: Node) -> Optional[Node]:
    if node.is_thread:
        return node.right
    else:
        if node.right:
            return RightThreadedBinaryTree.get_leftmost(node=node.right)
        # if node.right is None, it means no successor of the given node.
        return None

`get_predecessor` 的实现与二叉搜索树的前驱函数完全相同。

@staticmethod
def get_predecessor(node: Node) -> Optional[Node]:
    if node.left:
        return RightThreadedBinaryTree.get_rightmost(node=node.left)
    parent = node.parent
    while parent and node == parent.left:
        node = parent
        parent = parent.parent
    return parent

中序遍历

右单线索二叉树提供的一个好处是,我们无需辅助栈或递归即可进行中序遍历。算法如下:

  1. 从整个树的最左边节点开始。
  2. 如果右指针是线索,则跟随右指针;如果右指针不是线索,则转到子树的最左边节点。
  3. 重复步骤 2,直到右指针为空。

下图中的红色箭头演示了带有线索的中序遍历。

并且实现该函数时无需使用辅助栈或递归。

def inorder_traverse(self) -> traversal.Pairs:
    if self.root:
        current: Optional[Node] = self.get_leftmost(node=self.root)
        while current:
            yield (current.key, current.data)

            if current.is_thread:
                current = current.right
            else:
                if current.right is None:
                    break
                current = self.get_leftmost(current.right)

前序遍历

右单线索二叉树还提供了一种更简单的前序遍历方式,比中序遍历更简单。

  1. 从根节点开始。
  2. 如果左指针不为空,则转到左子节点。
  3. 如果左指针为空,则跟随线索转向右边。
  4. 重复步骤 2 和 3,直到右指针为空。

下图中的红色箭头显示了线索化前序遍历的路径。

前序遍历可以实现如下:

def preorder_traverse(self) -> traversal.Pairs:
    current = self.root
    while current:
        yield (current.key, current.data)

        if current.is_thread:
            # If a node is thread, it must have a right child.
            current = current.right.right  # type: ignore
        else:
            current = current.left

左单线索二叉搜索树

左单线索二叉树与右单线索二叉树是对称的。在左单线索二叉树中,如果任何节点的左指针为空,则该左指针是线索,指向该节点的中序前驱,并且 `is_thread` 变量设置为 `True`。左单线索二叉树的形象化如下图所示。

左单线索二叉树的类结构与右单线索二叉树几乎相同。唯一的区别是,左单线索二叉树提供简单的反中序遍历,而不是中序和前序遍历。

class LeftThreadedBinaryTree:

    def __init__(self) -> None:
        self.root: Optional[Node] = None

    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) -> Optional[Node]:
        …

    @staticmethod
    def get_predecessor(node: Node) -> Optional[Node]:
        …

    @staticmethod
    def get_height(node: Optional[Node]) -> int:
        …

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

Insert

`insert` 操作类似于右单线索二叉树。区别在于线索在左侧。

  1. 通过从根节点开始遍历树,并将新节点键与沿途每个节点的键进行比较,找到插入新节点的合适位置(即新节点的父节点)。向左子树遍历时,还要检查 `is_thread` 变量。如果该变量为 `True`,则表示我们已到达叶子节点,该叶子节点就是父节点。
  2. 找到父节点后,更新父节点的左指针(或右指针,取决于键值)指向新节点。
  3. 更新新节点的 parent 属性以指向父节点。
  4. 如果新节点是父节点的右子节点,则将新节点的左指针指向父节点,并将 `is_thread` 变量设置为 `True`。
  5. 如果新节点是父节点的左子节点,则将父节点的左指针(在插入之前,父节点的左指针必须是线索)复制到新节点的左指针,并将 `is_thread` 设置为 `True`。更新父节点的左指针指向新节点。

下图演示了节点插入的步骤。

实现如下:

def insert(self, key: Any, data: Any) -> None:
    new_node = Node(key=key, data=data)
    parent: Optional[Node] = None
    current: Optional[Node] = self.root

    while current:
        parent = current
        if new_node.key < current.key:
            # If the node is thread, meaning it's a leaf node.
            if current.is_thread:
                current = None
            else:
                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 tree is empty
    if parent is None:
        self.root = new_node
    elif new_node.key > parent.key:
        parent.right = new_node
        # Update thread
        new_node.left = parent
        new_node.is_thread = True

    else:
        # Update thread
        new_node.is_thread = parent.is_thread
        new_node.left = parent.left
        parent.is_thread = False
        # Parent's left must be set after thread update
        parent.left = new_node

搜索

搜索操作与右单线索二叉树类似,因此我们需要检查 `is_thread` 变量来确定我们是否到达了叶子节点。

  1. 从根节点开始遍历树,并将要查找的键与沿途每个节点的键进行比较。
  2. 如果键匹配,则找到了该节点。
  3. 如果在到达叶子节点后(如果 `is_thread` 为 `True`,也表示该节点是叶子节点)仍未找到匹配的键,则表示该键不存在于树中。

实现与右单线索二叉树的搜索非常相似。

def search(self, key: Any) -> Optional[Node]:
    current = self.root

    while current:
        if key == current.key:
            return current
        elif key < current.key:
            if current.is_thread is False:
                current = current.left
            else:
                break
        else:  # key > current.key:
            current = current.right
    return None

删除

同样,左单线索二叉树的删除与右单线索二叉树是对称的,也分为三种情况:无子节点、只有一个子节点、有两个子节点。

情况 1:无子节点

情况 2a:只有一个左子节点

情况 2b:只有一个右子节点

情况 3:有两个子节点

3.a 要删除节点的右子节点也是右子树中最左边的节点。

3.b 要删除节点的右子节点也有两个子节点。

被替换节点没有子节点

被替换节点只有一个右子节点:

与右单线索二叉树一样,我们也使用在二叉搜索树删除中使用的替换(transplant)技术来用要删除的节点替换子树。

获取高度

`get_height` 函数与右单线索二叉树的实现是对称的。

获取最左侧和最右侧节点

由于左单线索二叉树与右单线索二叉树是对称的,因此在尝试获取最左节点时,我们需要检查 `is_thread` 是否为 `True` 并检查左指针是否为空。

@staticmethod
def get_leftmost(node: Node) -> Node:
    current_node = node

    while current_node.left and current_node.is_thread is False:
        current_node = current_node.left
    return current_node

`get_rightmost` 的实现与二叉搜索树中的 `get_rightmost` 完全相同。

@staticmethod
def get_rightmost(node: Node) -> Node:
    current_node = node
    while current_node.right:
        current_node = current_node.right
    return current_node

前驱和后继

根据左单线索二叉树的定义:节点的空左指针指向其中序前驱。我们可以通过跟随线索来获取节点的“前驱”,如果节点的 `is_thread` 为 `True`。

@staticmethod
def get_predecessor(node: Node) -> Optional[Node]:
    if node.is_thread:
        return node.left
    else:
        if node.left:
            return LeftThreadedBinaryTree.get_rightmost(node=node.left)
        # if node.left is None, it means no predecessor of the given node.
        return None

`get_successor` 的实现与二叉搜索树的后继函数完全相同。

@staticmethod
def get_successor(node: Node) -> Optional[Node]:
    if node.right:
        return LeftThreadedBinaryTree.get_leftmost(node=node.right)
    parent = node.parent
    while parent and node == parent.right:
        node = parent
        parent = parent.parent
    return parent

反中序遍历

通过左线索,左单线索二叉树在执行反中序遍历时既不需要递归也不需要辅助栈。

  1. 从整个树的最右边节点开始。
  2. 如果左指针是线索,则跟随线索;如果左指针不是线索,则转到子树的最右边节点。
  3. 重复步骤 2,直到左指针为空。

下图中的红色箭头演示了带有线索的反中序遍历。

以下是实现:

def reverse_inorder_traverse(self) -> traversal.Pairs:
    if self.root:
        current: Optional[Node] = self.get_rightmost(node=self.root)
        while current:
            yield (current.key, current.data)

            if current.is_thread:
                current = current.left
            else:
                if current.left is None:
                    break
                current = self.get_rightmost(current.left)

测试

一如既往,我们应该尽可能为我们的代码编写单元测试。在这里,我们使用在“Python 系列:构建二叉搜索树”中创建的 `conftest.py` 文件中的 `basic_tree` 函数来测试我们的单线索二叉树。完整的单元测试请参见 test_single_threaded_binary_trees.py

分析

线索二叉树操作的运行时间与普通二叉搜索树的操作相同。

尽管右单线索二叉树提供了更简便的方式来获取节点的后继,左单线索二叉树提供了更直接的方式来获取节点的前驱,但线索并未在运行时间上节省太多。主要原因是这些函数仍然需要调用 `get_leftmost` 和 `get_rightmost` 函数,它们的平均运行时间为 O(lg n),最坏情况下的运行时间为 O(n)。

然而,线索确实在特定遍历方式上节省了空间复杂度。

示例

为二叉搜索树添加线索会使其实现更加复杂,但当遍历是关键需求,同时又需要考虑空间消耗时,它们可以提供一种解决方案。例如,我们想构建一个数据库,用户需要频繁地按升序和降序访问数据,但我们内存有限(即由于空间消耗考虑,无法使用辅助栈或递归)。在这种情况下,我们可以使用线索二叉树来实现用于升序和降序访问的索引。

from typing import Any

from forest.binary_trees import single_threaded_binary_trees
from forest.binary_trees import traversal

class MyDatabase:
    """Example using threaded binary trees to build an index."""

    def __init__(self) -> None:
        self._left_bst = single_threaded_binary_trees.LeftThreadedBinaryTree()
        self._right_bst = single_threaded_binary_trees.RightThreadedBinaryTree()

    def _persist(self, payload: Any) -> str:
        """Fake function pretent storing data to file system.

        Returns
        -------
        str
            Path to the payload.
        """
        return f"path_to_{payload}"

    def insert_data(self, key: Any, payload: Any) -> None:
        """Insert data.

        Parameters
        ----------
        key: Any
            Unique key for the payload
        payload: Any
            Any data
        """
        path = self._persist(payload=payload)
        self._left_bst.insert(key=key, data=path)
        self._right_bst.insert(key=key, data=path)

    def dump(self, ascending: bool = True) -> traversal.Pairs:
        """Dump the data.

        Parameters
        ----------
        ascending: bool
            The order of data.

        Yields
        ------
        Pairs
            The next (key, data) pair.
        """
        if ascending:
            return self._right_bst.inorder_traverse()
        else:
            return self._left_bst.reverse_inorder_traverse()

if __name__ == "__main__":

    # Initialize the database.
    my_database = MyDatabase()

    # Add some items.
    my_database.insert_data("Adam", "adam_data")
    my_database.insert_data("Bob", "bob_data")
    my_database.insert_data("Peter", "peter_data")
    my_database.insert_data("David", "david_data")

    # Dump the items in ascending order.
    print("Ascending...")
    for contact in my_database.dump():
        print(contact)

    print("\nDescending...")
    # Dump the data in decending order.
    for contact in my_database.dump(ascending=False):
        print(contact)

(完整示例可在 single_tbst_database.py 中找到。)

输出将如下所示:

Ascending...
('Adam', 'path_to_adam_data')
('Bob', 'path_to_bob_data')
('David', 'path_to_david_data')
('Peter', 'path_to_peter_data')

Descending...
('Peter', 'path_to_peter_data')
('David', 'path_to_david_data')
('Bob', 'path_to_bob_data')
('Adam', 'path_to_adam_data')

摘要

尽管添加线索会增加复杂性且不会提高运行时间性能,但线索二叉树利用了左指针或右指针的空闲空间,并提供了一种简单的方式来检索前驱或后继,以及一种无需使用栈或递归即可执行某些遍历的解决方案。当考虑空间复杂度且特定遍历(例如中序遍历)至关重要时,线索二叉树可以成为一种选择。

下一篇文章将构建双线索二叉树,它将结合单线索二叉树的优点,但也会更加复杂。

历史

  • 2021年4月3日:初始版本
© . All rights reserved.