跳转至

树:从层次结构到 B+ 树索引

覆盖:树为何存在、二叉树与遍历、BST、B-Tree/B+ 树与数据库索引;与 数组哈希表 对照 适用:Day 3 专题深读,或复习 DDIA Ch.3 B-Tree 时对照 方法:5W1H(为什么学、学什么、何时用、在哪用、谁在用、怎么用)

← 返回 总览 | 数组 | 哈希表 | DDIA Ch.3 存储与检索 | 7 天冲刺 Day 3


读前须知:这篇在讲什么?

复习 B-Tree 时,很多人卡在:

  1. :和「二叉树刷题」是一回事吗?
  2. BST:为什么中序遍历是有序的?
  3. B-Tree / B+ 树:和内存里的红黑树、MySQL 索引是什么关系?
  4. 选型:什么时候不用哈希、要用树?

下面按 直觉 → 二叉树(面试)→ BST → B+ 树(工程/DDIA)→ 对照表 → 模板 → 刷题 写。
你正在复习 B-Tree:优先看 §0、§四、§五,二叉树题再回看 §二、§三。


§0 先用生活例子建立直觉

0.1 树是什么?

数组 / 链表:一排或一串,只有「前一个 / 后一个」关系。

:每个节点可以有多个「孩子」,天然表示 层级

                    公司 CEO
                   /        \
              技术 VP        产品 VP
             /    \              |
         后端组   数据组      产品一组
  • 根(root):最上面,只有一个
  • 父 / 子:上下级
  • 叶子(leaf):没有孩子的节点
  • 深度 / 高度:从根到某节点的层数

为什么需要树:文件目录、组织架构、DOM、分类菜单——都不是「一条线」能舒服表达的。


0.2 二叉树 vs 二叉搜索树(BST)vs B-Tree(别混)

名字 约束 主要用途
二叉树 每个节点最多 2 个孩子 遍历、递归、DP 载体
BST 左 < 根 < 右,递归定义 有序、查找 O(log n)(平衡时)
B-Tree / B+ 树 多路平衡、节点多个 key、磁盘页友好 数据库索引、范围查询
二叉树:只规定「最多两个孩子」
BST:   还规定「左边都比根小,右边都比根大」
B+树:  一棵很「胖」的树,一层对应磁盘一页,叶子串成链表

0.3 和数组、哈希表怎么选?(一句话)

需求 更合适
按下标访问、连续扫描 数组
按 key 点查、计数、分组(无序) 哈希表
层次关系、第 K 小、范围 [a,b]、有序遍历
海量追加写、日志型 LSM(见 DDIA Ch.3

树的核心卖点:在 有序 前提下,查找 / 范围 / 顺序遍历 都是 O(log n) 量级(平衡树)。


一、为什么需要树?(Why)

1.1 线性结构的局限

结构 查找 表达层级
无序数组 O(n)
有序数组 + 二分 O(log n) 查找,插入 O(n)
链表 O(n)
哈希表 O(1) 平均点查,无序、无范围

:插入 + 查找都可做到 O(log n)(平衡时),还能 中序遍历得到有序序列,还能表达父子层级。

1.2 历史脉络(简表)

阶段 内容
数学 树、图论很早就有
1960s BST 作为抽象数据结构研究
1970s B-Tree(Bayer & McCreight),为 磁盘块 设计
工程 B+ 树成为 MySQL InnoDB、PostgreSQL 等索引主流

B-Tree 动机:磁盘一次读 一整页(如 16KB),希望一次 I/O 里尽量多 key、少指针跳转;二叉树太「瘦」,层数深,I/O 次数多。


二、二叉树(面试主战场)

2.1 四种遍历 — 各干什么

对同一棵树:

        1
       / \
      2   3
     / \
    4   5
遍历 顺序 记忆 典型用途
前序 根 → 左 → 右 1,2,4,5,3 复制树、序列化前缀
中序 左 → 根 → 右 4,2,5,1,3 BST 得有序序列
后序 左 → 右 → 根 4,5,2,3,1 删树、算子树信息(直径、路径和)
层序 按层 BFS [[1],[2,3],[4,5]] 最短层数、右视图

BST 中序有序的原因:左子树全小于根,右子树全大于根 → 递归下去全局有序。


2.2 递归三要素(每道树题都套)

  1. 终止node == null 返回什么(0、true、null)
  2. 递归:左、右子树各返回什么
  3. 合并:当前节点如何用左右结果(max、加、判断)

三种模式7 天冲刺 同款):

模式 信息流向 例子
自顶向下 根传参到叶 最大深度、路径和
自底向上 叶汇总到根 直径、最大路径和
判断型 整棵树是否满足条件 验证 BST、对称树

2.3 手把手:验证 BST(LC 98)

不能只看「左孩子 < 根 < 右孩子」,要 整条左子树 < 根 < 整条右子树

       5
      / \
     1   6
        / \
       4   7   ← 4 在右子树但 < 5,非法

做法:DFS 带区间 (lo, hi),到节点时检查 lo < val < hi,再传给左右子树。

boolean isValidBST(TreeNode root) {
    return dfs(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
boolean dfs(TreeNode n, long lo, long hi) {
    if (n == null) return true;
    if (n.val <= lo || n.val >= hi) return false;
    return dfs(n.left, lo, n.val) && dfs(n.right, n.val, hi);
}

2.4 层序遍历(LC 102)— BFS 模板

List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> ans = new ArrayList<>();
    if (root == null) return ans;
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);
    while (!q.isEmpty()) {
        int size = q.size();           // 当前层节点数
        List<Integer> level = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            TreeNode node = q.poll();
            level.add(node.val);
            if (node.left != null) q.offer(node.left);
            if (node.right != null) q.offer(node.right);
        }
        ans.add(level);
    }
    return ans;
}

关键for (int i = 0; i < size; i++)当前队列长度 卡一层,不要 while (!q.isEmpty()) 里无界 poll。


三、二叉搜索树(BST)

3.1 操作与复杂度(平衡时)

操作 平均
查找 O(log n)
插入 O(log n)
删除 O(log n)
中序遍历 O(n) 且 有序

退化:按 1,2,3,4… 顺序插入 → 变链表 → O(n)。工程用 AVL、红黑树 保持平衡;Java TreeMap 底层红黑树。

3.2 第 K 小(LC 230)

中序遍历第 K 个就是答案;或 BST 上「左子树 size + 1」走左/右。


四、B-Tree 与 B+ 树(你在复习的重点)

DDIA Ch.3:B+Tree 与 LSM 对照 对照阅读。

4.1 解决什么问题?

痛点 B-Tree 思路
二叉树索引层数太深 每个节点存 很多 key(多路),树变矮
磁盘按 读写 一个节点 ≈ 一页,一次 I/O 读一整节点
范围查询 WHERE id BETWEEN … 叶子链表 顺序扫(B+ 树)

不是面试里手写的那种「每个节点只有 2 个孩子」的二叉树,而是 多路平衡树


4.2 B-Tree vs B+ 树(记差异)

B-Tree B+ 树(InnoDB 常见)
数据存在哪 非叶节点也可能存 value 只在叶子存行/指针
叶子 无链表 叶子串成双向链表
范围扫描 一般 极强(沿叶子链扫)
单点查 可能在上层结束 通常要到叶子
         [10 | 20 | 30]          ← 非叶:只存 key(或 key+指针)
        /        |        \
   [...]      [...]      [10→row][20→row][30→row] ← 叶子:数据 + 链表 →

4.3 和 LSM、哈希 怎么选(复习 DDIA 时用)

结构 写入 点查 范围查询 典型
哈希 O(1) Redis、哈希索引
LSM 顺序写快 一般 RocksDB
B+ 树 随机页更新 O(log n) MySQL InnoDB

计费 / OLTP:按订单号查 + 按日期范围报表 → B+ 树索引非常常见。
纯埋点灌库:更偏 LSM / 列存 merge,不是经典 B+ 树 OLTP 模型。


4.4 为什么数据库少用「二叉」红黑树做磁盘索引?

  • 二叉树 扇出小(每个节点 2 路)→ 同样 key 数量 层数高 → 磁盘 I/O 次数多
  • B+ 树 扇出大(一页塞几百 key)→ 层数低 → 3~4 层 就能存亿级索引(量级直觉)

红黑树仍重要:内存里TreeMap、Java 8 哈希桶树化),不是磁盘主索引形态。


4.5 MVP 直觉:B+ 树查找(伪代码)

// 教学用:多路查找,不涉及页分裂/合并
Record search(BPlusNode root, int key) {
    BPlusNode node = root;
    while (!node.isLeaf) {
        int i = 0;
        while (i < node.keys.size() && key >= node.keys.get(i)) i++;
        node = node.children.get(i);   // 落到下一层
    }
    // 叶子:沿链表或页内二分找 key
    return node.findInLeaf(key);
}

分裂 / 合并:插入满页 → 分裂;删除导致页太空 → 合并或借位 —— 实现细节交给数据库,理解 保持平衡 + 页满 即可。


五、刷题 / 工程怎么选?(决策树)

拿到「树」相关题或需求
        │
        ├─ 题目给的是 TreeNode,问深度/路径/对称/构造?
        │       └─ 【二叉树 + 递归 / BFS】§二
        │
        ├─ 要求有序、第 K 小、验证 BST?
        │       └─ 【BST + 中序 / 区间 DFS】§三
        │
        ├─ 讨论 MySQL 索引、范围查询、磁盘?
        │       └─ 【B+ 树】§四,对照 DDIA
        │
        ├─ 只要 key 存在不存在、频次、分组?
        │       └─ 优先 【哈希表】,不是树
        │
        └─ 连续下标、子数组子串?
                └─ 【数组】技巧,不是树
技巧 一句话 代表题
DFS 前/中/后序 递归遍历 94, 226, 114
BFS 层序 队列按层 102, 199
自底向上 DFS 子树信息汇总 543, 124
BST 中序 / 区间 有序与验证 98, 230
构造树 前+中 / 后+中 + 哈希下标 105, 106

六、核心模板(Java)

class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode(int v) { val = v; }
}

// 1. 前序 DFS
void preorder(TreeNode root, List<Integer> out) {
    if (root == null) return;
    out.add(root.val);
    preorder(root.left, out);
    preorder(root.right, out);
}

// 2. 中序 DFS(BST 有序)
void inorder(TreeNode root, List<Integer> out) {
    if (root == null) return;
    inorder(root.left, out);
    out.add(root.val);
    inorder(root.right, out);
}

// 3. 最大深度(自顶向下)
int maxDepth(TreeNode root) {
    if (root == null) return 0;
    return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}

// 4. 直径(自底向上,后序思想)
int diameter(TreeNode root) {
    int[] ans = new int[1];
    height(root, ans);
    return ans[0];
}
int height(TreeNode n, int[] ans) {
    if (n == null) return 0;
    int L = height(n.left, ans);
    int R = height(n.right, ans);
    ans[0] = Math.max(ans[0], L + R);
    return Math.max(L, R) + 1;
}

// 5. 最近公共祖先(一般二叉树)
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) return root;
    TreeNode L = lowestCommonAncestor(root.left, p, q);
    TreeNode R = lowestCommonAncestor(root.right, p, q);
    if (L != null && R != null) return root;
    return L != null ? L : R;
}

七、必刷题目(15 题)

优先级 题目 技巧
⭐⭐⭐ 102 层序遍历 BFS
⭐⭐⭐ 104 最大深度 DFS
⭐⭐⭐ 226 翻转二叉树 DFS
⭐⭐⭐ 98 验证 BST 区间
⭐⭐⭐ 230 BST 第 K 小 中序
⭐⭐⭐ 236 最近公共祖先 DFS
⭐⭐⭐ 543 直径 后序型
⭐⭐⭐ 124 最大路径和 后序型
⭐⭐⭐ 105 前中构造二叉树 递归+哈希
⭐⭐ 101 对称二叉树 DFS
⭐⭐ 114 展开为链表 前序
⭐⭐ 199 右视图 BFS/DFS
⭐⭐ 112 路径总和 DFS
⭐⭐ 437 路径总和 III 前缀和+DFS
⭐⭐ 297 序列化与反序列化 BFS/DFS

八、小结(背这几句)

主题 记住这一句
树为何存在 表达 层级;在有序前提下 O(log n) 查找与范围
二叉树 面试载体;四种遍历 + 递归三模式
BST 左 < 根 < 右;中序 = 有序;平衡才 O(log n)
B+ 树 磁盘索引;多路、矮树、叶子链表 扫范围
和哈希 哈希点查快无序;树要 有序 / 范围 / 第 K 小
和 LSM LSM 写多读少追加;B+ 树 读多、范围、行级更新

复习 B-Tree 时:先画「根 → 非叶 key → 叶子数据+链表」,再对照 DDIA Ch.3 B+Tree框架总览

刷题口诀

  1. TreeNode + 深度/路径/对称 → DFS/BFS
  2. 有序 / 第 K 小 / 验证 → BST + 中序或区间
  3. 数据库索引 / 范围 → B+ 树,不是二叉

若仍懵:在纸上画一棵 4 节点的 BST,手写中序结果,再画一层「一页 3 个 key」的 B+ 树节点。