二叉搜索树与平衡树

从「左小右大」到「站得稳」。 上一章我们学了二叉树:每个节点最多两个孩子。如果再加上一条规则——左子树里所有值都小于根,右子树里所有值都大于根——就得到二叉搜索树(BST):查找、插入、删除都能沿着一条路径走,理想情况下是 O(log n)。但若一直按顺序插入 1、2、3、4…,树会退化成一条链,复杂度变成 O(n)。于是人们发明了平衡树:在保持「左小右大」的前提下,通过旋转等操作让树尽量「矮」,保证查找、插入、删除都在 O(log n)。本章会讲 BST 的完整实现,以及两种经典平衡树——AVL红黑树——的原理、图示与 Python 代码,并配有例题与扩展阅读。

一、二叉搜索树(BST)

二叉搜索树(Binary Search Tree,BST)是一棵二叉树,且满足:对任意节点,其左子树中所有节点的值 < 该节点值右子树中所有节点的值 > 该节点值。通常约定同一集合内无重复键(若有重复,可扩展为「左 ≤ 根 < 右」或挂在同一节点的链表上)。这一性质带来一个直接后果:中序遍历 BST 会得到有序序列

BST 示例:根 50,左子树上 12 < 25 < 35 < 50,右子树上 50 < 60 < 75 < 90

1.1 查找、插入、删除

查找:从根开始,若目标等于当前节点则找到;若小于当前值则进入左子树,否则进入右子树;若走到空则不存在。一次比较排除一整棵子树,理想情况下高度为 O(log n),故查找 O(log n)。

插入:先按查找走到底(若已存在可依约定处理);在「空位」处挂上新节点。插入后仍满足 BST 性质。

删除:分三种情况。(1)被删节点是叶:直接删。(2)只有一个孩子:用该孩子顶替被删节点。(3)有两个孩子:通常用后继(中序后继,即右子树中最左节点)或前驱(中序前驱,即左子树中最右节点)的值覆盖当前节点,再在子树中删除该后继/前驱(该节点必至多一个孩子),从而转化为情况 1 或 2。

顺序插入导致 BST 退化为链:查找、插入、删除都变成 O(n)

二、BST 的 Python 实现

下面给出带类型标注详细注释的 BST:节点类、查找、插入、删除(用后继覆盖),以及中序遍历。代码约定不存重复键,重复插入视为无操作。

from __future__ import annotations
from typing import Optional, List


class BSTNode:
    """BST 节点:键 key、左子 left、右子 right。"""

    def __init__(self, key: int) -> None:
        self.key: int = key
        self.left: Optional[BSTNode] = None
        self.right: Optional[BSTNode] = None


def bst_search(root: Optional[BSTNode], key: int) -> Optional[BSTNode]:
    """在 BST 中查找 key,返回对应节点或 None。"""
    if root is None or root.key == key:
        return root
    if key < root.key:
        return bst_search(root.left, key)
    return bst_search(root.right, key)


def bst_insert(root: Optional[BSTNode], key: int) -> BSTNode:
    """在 BST 中插入 key(不允许重复)。返回插入后的根。"""
    if root is None:
        return BSTNode(key)
    if key < root.key:
        root.left = bst_insert(root.left, key)
    elif key > root.key:
        root.right = bst_insert(root.right, key)
    # key == root.key:不插入,直接返回
    return root


def _bst_min_node(node: BSTNode) -> BSTNode:
    """返回以 node 为根的子树中的最小键节点(最左节点)。"""
    while node.left is not None:
        node = node.left
    return node


def bst_delete(root: Optional[BSTNode], key: int) -> Optional[BSTNode]:
    """在 BST 中删除键为 key 的节点。返回删除后的根。"""
    if root is None:
        return None
    if key < root.key:
        root.left = bst_delete(root.left, key)
        return root
    if key > root.key:
        root.right = bst_delete(root.right, key)
        return root
    # 找到待删节点 root
    if root.left is None:
        return root.right
    if root.right is None:
        return root.left
    # 有两个孩子:用后继(右子树最左)的值覆盖,再删后继
    successor = _bst_min_node(root.right)
    root.key = successor.key
    root.right = bst_delete(root.right, successor.key)
    return root


def bst_inorder(root: Optional[BSTNode]) -> List[int]:
    """中序遍历 BST,返回有序键列表。"""
    if root is None:
        return []
    return bst_inorder(root.left) + [root.key] + bst_inorder(root.right)

三、平衡树:AVL 与红黑树概览

为了让树高稳定在 O(log n),需要在插入、删除后做「平衡」:通过旋转调整结构。两种经典平衡树:

二者都能保证查找、插入、删除在 O(log n);AVL 更「严格平衡」、查找略优,红黑树旋转与变色次数通常更少、写多读少场景常用(如很多语言的标准库 map/set 底层)。

四、AVL 树

AVL 树(由 Adelson-Velsky 和 Landis 提出)是 BST,且对每个节点有:|平衡因子| ≤ 1,其中平衡因子 = 左子树高度 - 右子树高度。空树高度记为 -1,单节点高度 0。

4.1 平衡因子与失衡类型

插入或删除后,从受影响节点沿父指针向上,可能首次遇到平衡因子变为 ±2 的节点,该节点为「失衡点」。以该节点为根的子树的失衡可归纳为四类(设失衡节点为 A,较高的一侧子节点为 B):

AVL 单旋:右单旋把「左子 B」提为根,A 变成 B 的右子;左单旋对称

4.2 AVL 插入与旋转的 Python 实现

节点增加 height 字段(或每次递归时计算高度);插入后自底向上更新高度并检查平衡因子,若为 ±2 则按 LL/RR/LR/RL 执行对应旋转。下面给出带高度与平衡因子的 AVL 节点、高度更新、四种旋转及插入的完整逻辑。

from __future__ import annotations
from typing import Optional, List


class AVLNode:
    """AVL 节点:key、left、right、height(当前子树高度,空为 -1)。"""

    def __init__(self, key: int) -> None:
        self.key: int = key
        self.left: Optional[AVLNode] = None
        self.right: Optional[AVLNode] = None
        self.height: int = 0


def _height(node: Optional[AVLNode]) -> int:
    """空树高度 -1,否则返回 node.height。"""
    return -1 if node is None else node.height


def _update_height(node: AVLNode) -> None:
    """根据左右子高度更新 node 的高度。"""
    node.height = 1 + max(_height(node.left), _height(node.right))


def _balance_factor(node: AVLNode) -> int:
    """平衡因子 = 左高 - 右高。"""
    return _height(node.left) - _height(node.right)


def _rotate_right(root: AVLNode) -> AVLNode:
    """右单旋:当前根为 A,左子为 B;旋转后 B 为根,A 为 B 的右子。"""
    new_root = root.left
    root.left = new_root.right
    new_root.right = root
    _update_height(root)
    _update_height(new_root)
    return new_root


def _rotate_left(root: AVLNode) -> AVLNode:
    """左单旋:当前根为 A,右子为 B;旋转后 B 为根,A 为 B 的左子。"""
    new_root = root.right
    root.right = new_root.left
    new_root.left = root
    _update_height(root)
    _update_height(new_root)
    return new_root


def avl_insert(root: Optional[AVLNode], key: int) -> AVLNode:
    """AVL 插入 key,返回新根。插入后自底向上检查平衡并旋转。"""
    if root is None:
        return AVLNode(key)
    if key < root.key:
        root.left = avl_insert(root.left, key)
    elif key > root.key:
        root.right = avl_insert(root.right, key)
    else:
        return root

    _update_height(root)
    bf = _balance_factor(root)

    # LL:左子左高 → 右单旋
    if bf == 2 and _balance_factor(root.left) >= 0:
        return _rotate_right(root)
    # RR:右子右高 → 左单旋
    if bf == -2 and _balance_factor(root.right) <= 0:
        return _rotate_left(root)
    # LR:左子右高 → 先左旋左子,再右旋根
    if bf == 2 and _balance_factor(root.left) < 0:
        root.left = _rotate_left(root.left)
        return _rotate_right(root)
    # RL:右子左高 → 先右旋右子,再左旋根
    if bf == -2 and _balance_factor(root.right) > 0:
        root.right = _rotate_right(root.right)
        return _rotate_left(root)
    return root

五、红黑树

红黑树(Red-Black Tree)是 BST,每个节点带颜色(红或黑),并满足以下五条性质:

  1. 每个节点是红色或黑色。
  2. 根是黑色。
  3. 叶节点(NIL,空指针)视为黑色。
  4. 红色节点的两个子节点都是黑色(不能有连续红)。
  5. 从任意节点到其每个 NIL 叶的路径上,黑色节点数量相同(黑高一致)。

由性质 4 和 5 可证:从根到叶的路径长度最多相差 2 倍(一条路径全黑,另一条路径黑红相间),故树高 O(log n)。

红黑树:根 50 黑,25 红(红边),75 黑;满足「无连续红」与「黑高相等」

5.1 红黑树插入(思路)

新插入节点先着红色(这样不破坏黑高)。若父为黑,直接结束。若父为红,则违反「不连续红」,需要调整:通过变色旋转(与 AVL 类似的左旋/右旋),使根为黑、无连续红、黑高不变。具体分 uncle 为红 / 为黑且三角/线形等情形,经典教材与算法导论有完整分类。下面给出带父指针与颜色的节点定义,以及「插入后修复」的框架代码(仅示意 uncle 为红时向上变色)。

from __future__ import annotations
from typing import Optional
from enum import Enum


class Color(Enum):
    RED = 1
    BLACK = 2


class RBNode:
    """红黑树节点:key、color、left、right、parent(便于回溯修复)。"""

    def __init__(self, key: int, color: Color = Color.RED) -> None:
        self.key: int = key
        self.color: Color = color
        self.left: Optional[RBNode] = None
        self.right: Optional[RBNode] = None
        self.parent: Optional[RBNode] = None


def rb_insert_fixup(root: RBNode, z: RBNode) -> RBNode:
    """
    插入后修复:z 为新插入的红节点。若父为红则需调整。
    仅示例:当 uncle 为红时,将父与 uncle 变黑、祖父变红,再递归处理祖父。
    实际还需处理 uncle 为黑时的旋转(与 AVL 类似的左/右旋 + 变色)。
    """
    while z.parent is not None and z.parent.color == Color.RED:
        # 此处应区分父是祖父的左子还是右子,找 uncle,再分 uncle 红/黑
        # 若 uncle 为红:父、uncle 变黑,祖父变红,z = 祖父
        # 若 uncle 为黑:通过旋转 + 变色使当前子树根为黑
        break  # 简化:仅作框架,完整实现见算法导论或标准库源码
    root.color = Color.BLACK
    return root

5.2 AVL 与红黑树对比

对比项AVL红黑树
平衡条件左右子树高度差 ≤ 1黑高一致、无连续红
树高严格 O(log n),更矮约 2log(n+1),略高
查找略优(树更矮)略逊,仍 O(log n)
插入/删除最多 O(1) 次旋转,但可能沿路径多次旋转最多 O(1) 次旋转 + 若干变色
典型用途读多写少、对查找延迟敏感语言库 map/set、写多读少
AVL 与红黑树:平衡严格度、树高、适用场景对比

六、小结与选用场景

BST 通过「左小右大」实现有序集合上的查找、插入、删除;若输入随机,期望 O(log n),但顺序插入会退化。AVL 用平衡因子 ±1 与四种旋转保持严格平衡,查找最优。红黑树 用颜色与五条规则保证近似平衡,插入/删除时旋转与变色次数有界,工程中更常见。Python 标准库没有内置平衡 BST,若需要可选用 sortedcontainers 等第三方库;C++ 的 std::map、Java 的 TreeMap 多为红黑树实现。

小贴士: 面试与做题时,若只需「有序 + 增删查」,可先说 BST 再提平衡树;实现时通常写 BST 或 AVL 即可,红黑树完整实现较繁琐,能说清性质与插入思路即可。

七、例题与扩展

以下例题中假设二叉树节点类型为 TreeNode,包含 valleftright,与第 7 章约定一致。

例题 1:验证二叉搜索树

题目描述:给定一棵二叉树的根 root,判断其是否是一棵有效的二叉搜索树。有效条件:节点左子树中所有值 < 该节点值,右子树中所有值 > 该节点值。

输入root 可能为 None。
输出:True 表示是有效 BST,否则 False。

from typing import Optional


def is_valid_bst(root: Optional[TreeNode]) -> bool:
    """
    中序思路:BST 中序遍历严格递增。递归时传递当前子树允许的 (下界, 上界),
    若节点值不在开区间 (lo, hi) 内则无效。初始为 (-inf, +inf)。
    """

    def in_range(node: Optional[TreeNode], lo: float, hi: float) -> bool:
        if node is None:
            return True
        if not (lo < node.val < hi):
            return False
        return in_range(node.left, lo, node.val) and in_range(node.right, node.val, hi)

    return in_range(root, float("-inf"), float("inf"))

讲解:不能只判断「左子 < 根 < 右子」,因为整棵左子树都要 < 根。用上下界传递:进入左子时上界收紧为根值,进入右子时下界收紧为根值。一次遍历 O(n),空间 O(h)。

例题 2:BST 中第 K 小的元素

题目描述:给定 BST 的根 root 和整数 k(1 ≤ k ≤ 节点数),返回第 k 小的元素。

输入root 可能为 None;k 合法。
输出:第 k 小的键值。

from typing import Optional


def kth_smallest(root: Optional[TreeNode], k: int) -> int:
    """
    中序遍历 BST 得到有序序列,第 k 个即为第 k 小。
    迭代中序:用栈模拟,每次「向左走到底再弹栈访问」,计数到 k 即返回。
    """
    stack: list = []
    node = root
    count = 0
    while stack or node is not None:
        while node is not None:
            stack.append(node)
            node = node.left
        node = stack.pop()
        count += 1
        if count == k:
            return node.val
        node = node.right
    return -1  # k 合法时不会走到

讲解:中序迭代用栈「左根右」访问,第 k 次访问的节点即为第 k 小。时间 O(k) 最坏 O(n),空间 O(h)。若节点带「以我为根的子树节点数」,也可在 O(log n) 内二分得到第 k 小(扩展:顺序统计树)。

例题 3:将有序数组转换为二叉搜索树

题目描述:给定一个严格递增的整数数组 nums,将其转换为一棵高度平衡的二叉搜索树。高度平衡指任意节点的左右子树高度差不超过 1。

输入nums 长度 ≥ 1,严格递增。
输出:BST 的根节点(任意一种合法构造即可)。

from typing import Optional, List


def sorted_array_to_bst(nums: List[int]) -> Optional[TreeNode]:
    """
    有序数组的中序即自身。取中间元素为根,左半为左子树、右半为右子树,递归构造。
    这样左右子树节点数最多差 1,自然高度平衡。
    """

    def build(l: int, r: int) -> Optional[TreeNode]:
        if l > r:
            return None
        mid: int = (l + r) // 2
        root = TreeNode(nums[mid])
        root.left = build(l, mid - 1)
        root.right = build(mid + 1, r)
        return root

    return build(0, len(nums) - 1)

讲解:BST 的中序遍历是递增序列,因此数组中点作为根时,左半对应左子树、右半对应右子树。递归时区间 [l, r] 的中点作为根,[l, mid-1] 建左子树、[mid+1, r] 建右子树。每次取中点保证左右子树规模接近,树高 O(log n)。时间 O(n),空间 O(log n) 递归栈。

例题 4:BST 迭代器(中序逐个输出)

实现「仅用 O(h) 空间、next() 均摊 O(1)」的中序迭代器:用栈保存「尚未访问完的根」,每次 next() 时向左走到底、弹栈访问、再转向右子。与上面 kth_smallest 的迭代中序一致,只需把「计数到 k」改成「返回当前节点」并支持 hasNext()

from typing import Optional


class BSTIterator:
    """中序迭代器:栈中保存「待访问的根」,next 时左到底、弹、转右。"""

    def __init__(self, root: Optional[TreeNode]) -> None:
        self._stack: list = []
        self._push_left(root)

    def _push_left(self, node: Optional[TreeNode]) -> None:
        while node is not None:
            self._stack.append(node)
            node = node.left

    def next(self) -> int:
        cur = self._stack.pop()
        self._push_left(cur.right)
        return cur.val

    def has_next(self) -> bool:
        return len(self._stack) > 0

讲解:用栈维护「尚未访问完的根」;next() 时从栈顶取出当前根、访问,再将其右子「向左走到底」全部入栈,这样下次 next() 会按中序得到下一个节点。每个节点入栈、出栈各一次,均摊 O(1),空间 O(h)。