二叉搜索树:查找、插入与删除

摘要

二叉搜索树在普通二叉树的基础上增加了有序性:左子树节点值小于当前节点,右子树节点值大于当前节点。借助这一性质,可以在树高较低时高效完成查找、插入和删除。

本文介绍二叉搜索树的性质、递归与迭代查找、插入节点、删除节点、中序遍历和退化问题,并通过 Python 实现一个基础的二叉搜索树。

一、背景与问题

数组、链表和哈希表各有适用场景:

  • 数组支持快速随机访问。
  • 链表适合节点连接和局部插入。
  • 哈希表平均情况下支持快速等值查找。

但如果需要同时支持有序遍历、范围查询、查找、插入和删除,就需要一种能够维护顺序关系的数据结构。二叉搜索树通过节点之间的大小关系组织数据:

text 复制代码
          8
        /   \
       3     10
      / \      \
     1   6      14
        / \     /
       4   7   13

对这棵树进行中序遍历,可以得到有序序列:

text 复制代码
1, 3, 4, 6, 7, 8, 10, 13, 14

二、核心概念

1. 二叉搜索树性质

对于任意节点:

text 复制代码
左子树所有值 < 当前节点
右子树所有值 > 当前节点

如果允许重复值,需要额外定义重复值放左侧、右侧,或者在节点中保存计数。

2. 查找路径

查找目标值时:

  • 目标等于当前节点,查找成功。
  • 目标小于当前节点,进入左子树。
  • 目标大于当前节点,进入右子树。

每次比较都可以排除一侧子树。

3. 树高与复杂度

设树高为 h:

操作 复杂度
查找 O(h)
插入 O(h)
删除 O(h)
中序遍历 O(n)

平衡时 h 接近 log n,退化成链表时 h 接近 n。

4. 删除节点的三种情况

删除节点需要区分:

  1. 叶子节点:直接删除。
  2. 只有一个子节点:用子节点替代当前节点。
  3. 有两个子节点:使用右子树最小节点或左子树最大节点替代当前值,再删除替代节点。

第三种情况是删除操作的核心。

三、工作原理

1. 查找

text 复制代码
从根节点开始
  → 比较目标值
  → 相等:返回
  → 目标更小:进入左子树
  → 目标更大:进入右子树
  → 到达空节点:不存在

2. 插入

插入新值时沿查找路径向下,直到找到空位置:

text 复制代码
找到空子树
  → 创建新节点
  → 将新节点挂到父节点对应方向

插入后必须保持二叉搜索树性质。

3. 删除两个子节点的节点

例如删除节点 8:

text 复制代码
找到右子树最小节点 10
  → 用 10 替换 8
  → 在原右子树中删除 10

因为右子树最小值大于左子树所有节点且不大于右子树其他节点,所以替换后仍然满足有序性。

4. 中序遍历验证

二叉搜索树的中序遍历应该产生非递减序列。这个性质可以作为结构正确性的基础校验。

四、实战示例

1. 定义节点

python 复制代码
from __future__ import annotations

from dataclasses import dataclass


@dataclass
class Node:
    value: int
    left: Node | None = None
    right: Node | None = None

2. 定义二叉搜索树

python 复制代码
class BinarySearchTree:
    def __init__(self) -> None:
        self.root: Node | None = None

3. 插入节点

python 复制代码
class BinarySearchTree:
    def insert(self, value: int) -> None:
        self.root = self._insert(self.root, value)

    def _insert(self, node: Node | None, value: int) -> Node:
        if node is None:
            return Node(value)

        if value < node.value:
            node.left = self._insert(node.left, value)
        elif value > node.value:
            node.right = self._insert(node.right, value)

        return node

这里选择忽略重复值。另一种设计是给节点增加 count 字段。

4. 查找节点

python 复制代码
class BinarySearchTree:
    def contains(self, value: int) -> bool:
        current = self.root

        while current is not None:
            if value == current.value:
                return True
            if value < current.value:
                current = current.left
            else:
                current = current.right

        return False

迭代查找不会增加 Python 的递归调用深度。

5. 中序遍历

python 复制代码
class BinarySearchTree:
    def inorder(self) -> list[int]:
        result: list[int] = []

        def visit(node: Node | None) -> None:
            if node is None:
                return
            visit(node.left)
            result.append(node.value)
            visit(node.right)

        visit(self.root)
        return result

6. 查找最小节点

python 复制代码
class BinarySearchTree:
    def _minimum(self, node: Node) -> Node:
        current = node
        while current.left is not None:
            current = current.left
        return current

从一个子树中不断向左走,就能找到该子树的最小值。

7. 删除节点

python 复制代码
class BinarySearchTree:
    def delete(self, value: int) -> None:
        self.root = self._delete(self.root, value)

    def _delete(self, node: Node | None, value: int) -> Node | None:
        if node is None:
            return None

        if value < node.value:
            node.left = self._delete(node.left, value)
            return node

        if value > node.value:
            node.right = self._delete(node.right, value)
            return node

        if node.left is None:
            return node.right
        if node.right is None:
            return node.left

        successor = self._minimum(node.right)
        node.value = successor.value
        node.right = self._delete(
            node.right,
            successor.value,
        )
        return node

删除有两个子节点的节点时,使用右子树的最小值作为后继节点。

8. 使用示例

python 复制代码
tree = BinarySearchTree()

for value in [8, 3, 10, 1, 6, 14, 4, 7, 13]:
    tree.insert(value)

print(tree.inorder())
print(tree.contains(7))
print(tree.contains(9))

tree.delete(3)
print(tree.inorder())

删除 3 后,树仍然应保持中序有序。

9. 验证搜索树性质

python 复制代码
def is_bst(
    node: Node | None,
    lower: int | None = None,
    upper: int | None = None,
) -> bool:
    if node is None:
        return True

    if lower is not None and node.value <= lower:
        return False
    if upper is not None and node.value >= upper:
        return False

    return is_bst(node.left, lower, node.value) and is_bst(
        node.right,
        node.value,
        upper,
    )

只检查每个节点和直接子节点的关系是不够的,必须把祖先节点的上下界传递给整棵子树。

10. 查找范围

python 复制代码
def range_values(
    node: Node | None,
    low: int,
    high: int,
) -> list[int]:
    result: list[int] = []

    def visit(current: Node | None) -> None:
        if current is None:
            return

        if current.value > low:
            visit(current.left)

        if low <= current.value <= high:
            result.append(current.value)

        if current.value < high:
            visit(current.right)

    visit(node)
    return result

利用有序性,可以跳过不可能落在范围内的子树。

五、常见问题与实践建议

1. 二叉搜索树一定比哈希表快吗?

不一定。哈希表擅长平均等值查找,二叉搜索树还能支持有序遍历和范围查询。选择取决于是否需要顺序关系和稳定的最坏情况性能。

2. 为什么二叉搜索树会退化?

如果按递增顺序插入数据,树可能变成:

text 复制代码
1
 \
  2
   \
    3
     \
      4

这时查找复杂度从接近 O(log n) 降为 O(n)。

3. 如何处理重复值?

需要提前定义策略:

  • 忽略重复值。
  • 重复值统一放左子树。
  • 重复值统一放右子树。
  • 节点中保存出现次数。

实现查找、删除和验证时必须保持策略一致。

4. 删除操作为什么容易出错?

因为要同时处理叶子、单子节点和双子节点三类情况,还要正确更新父节点引用。建议为每类情况单独编写测试。

5. 递归实现适合生产环境吗?

树高可控时可以使用递归;输入顺序不可控或数据规模较大时,应考虑迭代实现或直接使用成熟的平衡树容器。

六、进阶思考

1. 平衡二叉搜索树

AVL 树和红黑树通过旋转保持树高,避免退化。它们能够在查找、插入和删除之间取得稳定的 O(log n) 性能。

2. 范围查询

二叉搜索树的有序性使范围查询更自然。数据库索引和内存有序结构经常利用类似思想。

3. 迭代器

可以使用显式栈实现中序迭代器,每次返回下一个最小值,而不必一次性生成完整列表。这适合大树和流式处理。

4. 并发访问

多线程读写树结构时,需要考虑同步和一致性。简单加锁可以保护结构,但会降低并发;读多写少场景可以研究不可变结构或专门的并发容器。

5. 持久化

如果树需要持久化,不能只保存节点值,还要保存左右关系、重复值计数、版本和数据校验信息。

结论

二叉搜索树通过左小右大的性质维护有序数据,查找和插入沿树高方向执行,删除需要处理三类节点情况。中序遍历可以验证结构是否有序,范围查询可以利用子树边界剪枝。

二叉搜索树的性能高度依赖树形是否平衡。需要稳定性能时,应使用平衡树、数据库索引或成熟的有序容器,而不是假设普通二叉搜索树永远接近 O(log n)。

参考资料

  1. Introduction to Algorithms:https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/
  2. Open Data Structures:https://opendatastructures.org/
  3. Python 官方文档:https://docs.python.org/3/
相关推荐
HRTOS1 小时前
HRTOS 内核 BUG 修复:任务等待对象未初始化导致的异常唤醒问题
单片机·嵌入式硬件·算法·51单片机
XMAIPC_Robot1 小时前
国产FPGA替代Xilinx在储能电力采集场景可行性评估与移植经验
人工智能·算法·机器学习·fpga开发·rk3588+fpga
开开心心就好2 小时前
一元二次方程求解工具,答案带详细步骤
java·服务器·开发语言·算法·pdf·ocr·perl
Logic1012 小时前
C语言/数据结构位运算题解:异或XOR找出套碗游戏中的“幸运碗“——只出现一次的数字
c语言·数据结构·算法·数组·位运算·时间复杂度·空间复杂度
yi0112 小时前
LeetCode 3. 无重复字符的最长子串|不定长滑动窗口:什么时候更新最大值?
笔记·python·算法·leetcode·双指针·滑动窗口
y1su2 小时前
Leetcode 单调栈模板
java·开发语言·数据结构·算法·leetcode
索尔~古德曼3 小时前
CF(1400-1400&1300-1500)
算法
爱刷碗的苏泓舒3 小时前
泰勒级数与泰勒展开:GNSS卫地距线性化、Jacobian矩阵与Hessian矩阵
线性代数·算法·矩阵·gnss·雅可比矩阵·卫地距线性化·海森矩阵
垆边人似月.3 小时前
华为题:每日温度
数据结构·c++·算法