树与二叉搜索树 — 从零精通算法与数据结构——Google 面试系统备战 第5篇

第5章:树与二叉搜索树

本章目标

读完本章你会:

  • 手写 BST 的插入、查找、删除,理解 successor/predecessor 的含义
  • 用递归和迭代两种方式实现四种树遍历
  • 理解 AVL 旋转和红黑树的自平衡思想
  • 解决 LCA(最近公共祖先)、序列化/反序列化等经典树问题
  • 理解 std::map 为什么是 O(log n) —— 红黑树保证的

知识讲解

从一个生活例子开始

你的家族有一本族谱。从你太爷爷开始,往下画:

        太爷爷
       /      \
    爷爷      二爷爷
   /    \       /   \
 爸爸   叔叔   堂叔   堂姑
  |
 你

这就是一棵树。每个节点只能有一个父节点(你不会同时有两个爸爸),但可以有多个子节点。

二叉搜索树则在族谱上加了规则:所有人按年龄排:左子树都比我小,右子树都比我大。 这样找一个人不用翻全本——从根开始,每次只走一条路。

但如果族谱是"每代只有一个孩子"——所有人排成一条线——那又退化成了链表。这就是为什么需要平衡树:保证树"长得矮胖",让查找路径始终短。

工作原理

5.1 树的基础概念

术语 含义
最顶层的节点(太爷爷)
叶子 没有子节点的节点(你)
高度 从根到最远叶子的边数
深度 从根到该节点的边数
一个节点的子节点数
完全二叉树 除最后一层外每层都满,最后一层左对齐

5.2 二叉树遍历

四种遍历方式的本质差异是访问根节点的时机

        A
      /   \
     B     C
    / \   / \
   D   E F   G

前序(根-左-右): A B D E C F G → 用于复制树(先建根)
中序(左-根-右): D B E A F C G → BST 中序 = 升序输出
后序(左-右-根): D E B F G C A → 用于删除树(先删子)
层序(BFS):      A B C D E F G → 按层访问

递归实现很干净,但面试中常被要求写迭代版本——展示你对栈/队列的理解:

// 中序遍历——迭代版(栈模拟递归)
void InorderIterative(TreeNode* root) {
  std::stack<TreeNode*> st;
  TreeNode* curr = root;
  while (curr != nullptr || !st.empty()) {
    // 一路向左入栈
    while (curr != nullptr) {
      st.push(curr);
      curr = curr->left;
    }
    curr = st.top(); st.pop();
    Visit(curr);          // 访问节点
    curr = curr->right;   // 转向右子树
  }
}

5.3 BST 操作详解

查找: 从根开始,target < 当前 → 往左,target > 当前 → 往右。O(h),h 是树高度。

插入: 跟查找一样找到"应该放的位置",然后挂上去。O(h)。

删除——最复杂的操作,三种情况:

情况 做法
叶子节点(无子) 直接删
只有一个子 子节点替上来
有两个子 找后继(右子树最小值)替换,然后删除后继(必然属于前两种情况)

为什么 successor 是右子树的最小值? 中序遍历的顺序是 左 → 根 → 右。删掉"根"后,要保持有序性,"根"的值必须被"比它大的最小值"取代——正是右子树的最左节点。

5.4 平衡树:为什么需要旋转

退化的 BST(链表):  平衡的 BST:
    1                   4
     \                /   \
      2              2     6
       \            / \   / \
        3          1   3 5   7
         \
          4

AVL 树的思想: 每个节点维护"左右子树高度差"(平衡因子),当 |平衡因子| > 1 时旋转修复。

四种旋转情况:

LL(左-左): 右旋     RR(右-右): 左旋      LR(左-右): 左旋+右旋    RL(右-左): 右旋+左旋
    z                z                       z                       z
   /                / \                     /                       / \
  y       →        y   T4     → 旋转回平衡   y       →              y   T4
 /
x

红黑树的思想: 不用维护数字值(高度差),而是给节点染色 + 维护五条性质(根黑、叶黑、红节点的父必黑、黑高度相等)。保证了树高度 ≤ 2·log₂(n+1),从而 O(log n)。这是 C++ std::map 的底层实现。

面试实话: 90% 的树面试题不需要手写旋转——考察的是"你理解平衡树保证了什么性质"和"如何用 BST 解决问题"。但了解 AVL 旋转的思想有助于在面试官追问时展示深度。

5.5 经典树问题框架

LCA(最近公共祖先):

// 递归解法——利用 BST 的有序性质
TreeNode* LowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
  // 两个都在左边 → LCA 也在左边
  if (p->val < root->val && q->val < root->val)
    return LCA(root->left, p, q);
  // 两个都在右边 → LCA 也在右边
  if (p->val > root->val && q->val > root->val)
    return LCA(root->right, p, q);
  // 一个在左一个在右(或一个是根)→ 当前就是 LCA
  return root;
}

序列化/反序列化: 用某种遍历顺序记录树的结构(包括 null 标记),使能够从文件重建。这是 LeetCode 高难题,也是 Google 的经典问题——因为它同时考察遍历、递归、和边界处理。


代码实战

在 algo_toolkit 中添加 bst.h——实现 BST 的完整操作。

include/algo/bst.h

#ifndef ALGO_BST_H_
#define ALGO_BST_H_

#include <functional>
#include <memory>
#include <optional>
#include <vector>

namespace algo {

// BST 节点——用 unique_ptr 表达所有权
template <typename T>
struct BstNode {
  T value;
  std::unique_ptr<BstNode> left;
  std::unique_ptr<BstNode> right;

  explicit BstNode(T val) : value(std::move(val)) {}
};

// BST 类——封装树操作
template <typename T>
class BinarySearchTree {
 public:
  // 插入。返回 true 表示新插入,false 表示值已存在(更新)
  bool Insert(const T& value);

  // 查找。返回指向值的指针,未找到返回 nullptr(指针在树存活期间有效)
  const T* Find(const T& value) const;

  // 删除。返回 true 表示成功删除
  bool Remove(const T& value);

  // 四种遍历,结果写入 output
  void Inorder(std::vector<T>& output) const;
  void Preorder(std::vector<T>& output) const;
  void Postorder(std::vector<T>& output) const;

  // 查找最小值/最大值
  std::optional<T> Min() const;
  std::optional<T> Max() const;

  // 后继(比 value 大的最小节点值)
  std::optional<T> Successor(const T& value) const;

  bool empty() const { return root_ == nullptr; }
  std::size_t size() const { return size_; }

 private:
  // 递归辅助函数
  void InorderTraverse(const std::unique_ptr<BstNode<T>>& node,
                       std::vector<T>& output) const;

  // 查找并返回节点指针的引用(允许修改)
  BstNode<T>* FindNode(const T& value);
  const BstNode<T>* FindNodeConst(const T& value) const;

  // 找最小节点
  BstNode<T>* FindMin(BstNode<T>* node);

  std::unique_ptr<BstNode<T>> root_;
  std::size_t size_ = 0;
};

}  // namespace algo

#include "algo/bst_impl.h"

#endif  // ALGO_BST_H_

include/algo/bst_impl.h(核心实现摘要)

#ifndef ALGO_BST_IMPL_H_
#define ALGO_BST_IMPL_H_

namespace algo {

template <typename T>
bool BinarySearchTree<T>::Insert(const T& value) {
  if (!root_) {
    root_ = std::make_unique<BstNode<T>>(value);
    ++size_;
    return true;
  }

  BstNode<T>* curr = root_.get();
  while (true) {
    if (value == curr->value) return false;  // 重复值:BST 不允许
    if (value < curr->value) {
      if (!curr->left) {
        curr->left = std::make_unique<BstNode<T>>(value);
        ++size_;
        return true;
      }
      curr = curr->left.get();
    } else {
      if (!curr->right) {
        curr->right = std::make_unique<BstNode<T>>(value);
        ++size_;
        return true;
      }
      curr = curr->right.get();
    }
  }
}

template <typename T>
bool BinarySearchTree<T>::Remove(const T& value) {
  // 找到待删节点及其父节点
  BstNode<T>* parent = nullptr;
  BstNode<T>* curr = root_.get();

  while (curr && curr->value != value) {
    parent = curr;
    curr = (value < curr->value) ? curr->left.get() : curr->right.get();
  }
  if (!curr) return false;  // 未找到

  // 情况 3:两个子节点 → 找后继替换
  if (curr->left && curr->right) {
    BstNode<T>* succ_parent = curr;
    BstNode<T>* succ = curr->right.get();
    while (succ->left) {
      succ_parent = succ;
      succ = succ->left.get();
    }
    curr->value = succ->value;  // 用后继的值覆盖
    // 现在要删的是后继节点(必然属于情况 1 或 2)
    parent = succ_parent;
    curr = succ;
  }

  // 情况 1 和 2:取 curr 唯一的子节点(或 nullptr)
  std::unique_ptr<BstNode<T>> child =
      curr->left ? std::move(curr->left) : std::move(curr->right);

  // 挂到父节点上
  // parent == nullptr 表示要删除的是根节点——此时用 root_ 承接
  // 获取父节点中指向 curr 的那个指针(left 或 right)的引用
  auto& parent_ptr = (!parent) ? root_ :
    (parent->left.get() == curr) ? parent->left : parent->right;
  parent_ptr = std::move(child);
  --size_;
  return true;
}

}  // namespace algo

#endif  // ALGO_BST_IMPL_H_

本章小结

  1. 树是用指针(或引用)组织的层级数据结构,每个节点最多一个父节点
  2. BST 的查找/插入/删除都是 O(h),h 是树高——平衡时 O(log n),退化时 O(n)
  3. 四种遍历的区别是访问根的时机:前序先根、中序中间、后序最后、层序逐层
  4. 删除有两个子的节点 = 找后继替换 + 删除后继
  5. AVL 通过旋转修复不平衡,红黑树通过染色 + 少量旋转保证 O(log n)
  6. C++ std::map = 红黑树,Java TreeMap = 红黑树

关键术语

术语 释义
BST 二叉搜索树:左小右大的二叉树,支持 O(log n) 动态集合操作
AVL 树 第一个自平衡 BST(1962),严格保持高度差 ≤ 1
红黑树 宽松平衡 BST,通过染色规则保证 O(log n),插入最多 3 次旋转
旋转 在不改变中序遍历顺序的前提下重排子树节点 —— 平衡树的原子操作
LCA 最近公共祖先,树中离两个节点最近的共同祖先
后继/前驱 中序遍历中的下一个/上一个节点
posted @ 2026-06-22 01:09  Yobeeo  阅读(4)  评论(0)    收藏  举报