从二叉树到 B+ 树:一文搞懂工程中「树」的演化之道

本文系统梳理「树」这一核心数据结构,从基础概念到工程实践,全部代码用 Go 实现,带你搞懂为什么不同的场景需要不同的树。


一、为什么我们需要树?

在接触树之前,我们已经学过了数组和链表,也知道了散列表(Hash Table)可以在 O(1) 时间内完成查找。那么问题就来了:

有了如此高效的散列表,为什么还需要二叉树?

这是数据结构与算法中一个经典问题,也是很多面试中的高频考点。

散列表的局限

散列表虽然查询快,但它有几个硬伤:

  1. 散列冲突:当数据量增大时,冲突不可避免,性能会退化
  2. 无序性 :散列表中的数据是乱序的,不支持范围查找
  3. 扩容代价:动态扩容时需要 rehash,时间开销大
  4. 内存占用:为了保证较低的装载因子,通常需要预留更多空间

而树结构,尤其是二叉查找树(BST),天然支持有序存储,可以非常方便地进行范围查询(比如查找某个区间内的所有数据),这是散列表无法替代的。


二、树的基本概念:先搞清楚这些术语

在深入各种树之前,先把基础术语梳理清楚。概念清晰了,后面的理解才会顺畅。

2.1 什么是树?

树(Tree)是一种非线性 的数据结构,它由节点(Node)和边(Edge)组成,用来模拟具有层级关系的数据。

生活中常见的树状结构:公司的组织架构、文件系统的目录结构、HTML 的 DOM 树......

2.2 核心术语一览

术语 英文 含义 类比
根节点 Root 树的最顶端节点,没有父节点 公司的 CEO
父节点 Parent 某个节点的上层节点 你的直属领导
子节点 Child 某个节点的下层节点 你的下属
兄弟节点 Sibling 具有相同父节点的节点 你的同事
叶子节点 Leaf 没有子节点的节点 基层员工
内部节点 Internal Node 非叶子节点(有子节点) 中层管理者
节点的度 Degree 该节点拥有的子节点个数 下属人数
树的度 Tree Degree 树中所有节点度的最大值 公司最多下属人数
节点的深度 Depth 从根节点到该节点经过的边数 层级(CEO 是 0 层)
节点的高度 Height 从该节点到最远叶子节点的边数 该节点往下管多少层
树的高度 Tree Height 根节点的高度,即树的最大深度 公司总层级
层(Level) Level 根为第 1 层,往下逐层递增 第几层组织架构

2.3 一张图看懂所有概念

css 复制代码
                    A          ← 根节点(Root),深度 0,高度 3
                   /|\
                  / | \
                 B  C  D       ← B、C、D 是 A 的子节点
                / \     \      ← B 的度是 2,D 的度是 1
               E   F     G     ← E、F 是 B 的子节点,也是兄弟节点
              /              ← E 的度是 1
             H                ← H 是叶子节点(Leaf)
  • A 的深度 :0,A 的高度:3
  • B 的深度 :1,B 的高度:2
  • H 的深度 :3,H 的高度:0(叶子节点高度为 0)
  • 这棵树的度:3(A 有 3 个子节点,是最大值)
  • 这棵树的高度:3

2.4 树 vs 图:千万别混淆

树有一个非常重要的性质:

树是「无环」的连通图,任意两个节点之间有且只有一条路径。

如果出现了环,那就不是树,而是图了。这也是面试中常考的区分点。

2.5 二叉树 vs 多叉树

根据一个节点最多能有多少个子节点,树可以分为:

类型 定义 例子
二叉树 每个节点最多有 2 个子节点 二叉查找树、AVL 树、红黑树
多叉树 每个节点可以有多个子节点 B 树、B+ 树、Trie 树

三、二叉树基础:Go 代码全实现

3.1 二叉树的分类

二叉树(Binary Tree)是每个节点最多有两个子节点的树,分别称为左子树右子树

根据节点的排列方式,二叉树又分为几种特殊形态:

类型 定义 特点
满二叉树 除叶子节点外,每个节点都有左右两个子节点,且所有叶子都在同一层 节点数 = 2^h - 1
完全二叉树 除最后一层外,其他层都是满的;最后一层的节点都靠左排列 适合用数组存储
二叉查找树(BST) 左子树所有值 < 根节点 < 右子树所有值 支持高效查找
平衡二叉树 左右子树高度差不超过 1 防止退化成链表

满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。

3.2 存储方式:链式 vs 数组

存储方式 适用场景 特点
链式存储 普通二叉树 每个节点包含 ValLeftRight 指针,灵活但占用额外空间
数组存储 完全二叉树 按层序存储,父节点 i 的左右子节点分别在 2i+12i+2

什么样的二叉树适合用数组存储?

答案是:完全二叉树

因为完全二叉树除了最后一层,其他层都是满的,最后一层的节点都靠左排列,这样用数组存储不会浪费空间。而普通的二叉树如果用数组存储,可能会产生大量空洞,浪费内存。

这也是为什么(一种完全二叉树)通常用数组来实现。

3.3 二叉树的遍历(Go 实现)

首先定义节点结构:

go 复制代码
package main

import "fmt"

// TreeNode 二叉树节点
type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

前序遍历:根 -> 左 -> 右

go 复制代码
func preOrder(root *TreeNode) {
    if root == nil {
        return
    }
    fmt.Printf("%d ", root.Val)
    preOrder(root.Left)
    preOrder(root.Right)
}

中序遍历:左 -> 根 -> 右(BST 中序遍历即有序序列)

go 复制代码
func inOrder(root *TreeNode) {
    if root == nil {
        return
    }
    inOrder(root.Left)
    fmt.Printf("%d ", root.Val)
    inOrder(root.Right)
}

后序遍历:左 -> 右 -> 根

go 复制代码
func postOrder(root *TreeNode) {
    if root == nil {
        return
    }
    postOrder(root.Left)
    postOrder(root.Right)
    fmt.Printf("%d ", root.Val)
}

层序遍历(BFS)

go 复制代码
func levelOrder(root *TreeNode) {
    if root == nil {
        return
    }
    queue := []*TreeNode{root}
    for len(queue) > 0 {
        node := queue[0]
        queue = queue[1:]
        fmt.Printf("%d ", node.Val)
        if node.Left != nil {
            queue = append(queue, node.Left)
        }
        if node.Right != nil {
            queue = append(queue, node.Right)
        }
    }
}

遍历的记忆口诀

  • 前序:根左右(先访问根)
  • 中序:左根右(BST 中序 = 有序序列)
  • 后序:左右根(先处理子树,适合释放内存)
  • 层序:一层一层来(BFS,适合找最短路径)

四、二叉查找树(BST):Go 实现增删查

二叉查找树是二叉树最重要的应用形式,它满足:

  • 左子树所有节点的值 < 根节点的值
  • 右子树所有节点的值 > 根节点的值
  • 左右子树也是二叉查找树

BST 的查找、插入、删除时间复杂度都是 O(树的高度)

4.1 BST 查找

go 复制代码
func searchBST(root *TreeNode, target int) *TreeNode {
    if root == nil || root.Val == target {
        return root
    }
    if target < root.Val {
        return searchBST(root.Left, target)
    }
    return searchBST(root.Right, target)
}

4.2 BST 插入

go 复制代码
func insertBST(root *TreeNode, val int) *TreeNode {
    if root == nil {
        return &TreeNode{Val: val}
    }
    if val < root.Val {
        root.Left = insertBST(root.Left, val)
    } else if val > root.Val {
        root.Right = insertBST(root.Right, val)
    }
    return root
}

4.3 BST 删除

删除是 BST 中最复杂的操作,分三种情况:

  1. 删除叶子节点:直接删除
  2. 删除只有一个子节点的节点:用子节点替代
  3. 删除有两个子节点的节点:用右子树的最小值(或左子树的最大值)替代
go 复制代码
func deleteBST(root *TreeNode, val int) *TreeNode {
    if root == nil {
        return nil
    }
    if val < root.Val {
        root.Left = deleteBST(root.Left, val)
    } else if val > root.Val {
        root.Right = deleteBST(root.Right, val)
    } else {
        if root.Left == nil {
            return root.Right
        }
        if root.Right == nil {
            return root.Left
        }
        minNode := root.Right
        for minNode.Left != nil {
            minNode = minNode.Left
        }
        root.Val = minNode.Val
        root.Right = deleteBST(root.Right, minNode.Val)
    }
    return root
}

4.4 BST 的致命缺陷:退化成链表

但是! 如果插入的数据是有序的(比如 1, 2, 3, 4, 5),BST 会退化成一条链表,查找复杂度退化为 O(n)

markdown 复制代码
    1              3
     \           / \
      2    →    2   4
       \       /     \
        3     1       5
         \
          4
           \
            5
            
  退化成链表        平衡的理想形态

这就引出了下一个关键问题:如何让树保持平衡?


五、BBST(平衡二叉查找树):解决 BST 的退化问题

5.1 什么是 BBST?

BBST 全称 Balanced Binary Search Tree ,即平衡二叉查找树 。它是一类特殊的二叉查找树,通过某种机制自动维持树的平衡 ,确保树的高度始终保持在 O(log n) ,从而保证查找、插入、删除操作的时间复杂度稳定为 O(log n)

BBST 不是一个具体的树,而是一类树的统称。 常见的 BBST 包括:

  • AVL 树(严格平衡)
  • 红黑树(近似平衡)
  • Treap(树堆,结合 BST 和堆)
  • Splay 树(伸展树,自调整)

5.2 BBST 的核心思想:旋转

BBST 维持平衡的核心武器是旋转(Rotation)。旋转操作可以在不改变 BST 性质(左小右大)的前提下,调整树的结构,降低树的高度。

左旋(Left Rotation)

当某个节点的右子树过高时,需要左旋:

css 复制代码
    x              y
   / \            / \
  a   y    →     x   c
     / \        / \
    b   c      a   b
go 复制代码
// 左旋:解决右子树过高
func leftRotate(x *TreeNode) *TreeNode {
    y := x.Right
    b := y.Left
    y.Left = x
    x.Right = b
    return y
}

右旋(Right Rotation)

当某个节点的左子树过高时,需要右旋:

css 复制代码
      y            x
     / \          / \
    x   c   →    a   y
   / \              / \
  a   b            b   c
go 复制代码
// 右旋:解决左子树过高
func rightRotate(y *TreeNode) *TreeNode {
    x := y.Left
    b := x.Right
    x.Right = y
    y.Left = b
    return x
}

双旋:LR 型和 RL 型

有时候单次旋转不够,需要两次旋转:

  • LR 型(左子树的右子树过高):先对左子树左旋,再对根右旋
  • RL 型(右子树的左子树过高):先对右子树右旋,再对根左旋
go 复制代码
// LR 型:先左旋后右旋
func leftRightRotate(z *TreeNode) *TreeNode {
    z.Left = leftRotate(z.Left)
    return rightRotate(z)
}

// RL 型:先右旋后左旋
func rightLeftRotate(z *TreeNode) *TreeNode {
    z.Right = rightRotate(z.Right)
    return leftRotate(z)
}

5.3 平衡因子

如何判断一棵树是否平衡?通常使用平衡因子(Balance Factor)

平衡因子 = 左子树高度 - 右子树高度

go 复制代码
func height(node *TreeNode) int {
    if node == nil {
        return 0
    }
    leftH := height(node.Left)
    rightH := height(node.Right)
    if leftH > rightH {
        return leftH + 1
    }
    return rightH + 1
}

func getBalance(node *TreeNode) int {
    if node == nil {
        return 0
    }
    return height(node.Left) - height(node.Right)
}

当平衡因子的绝对值超过某个阈值时(AVL 树是 1),就需要通过旋转来恢复平衡。

5.4 BBST 的完整分类

BBST 类型 平衡策略 特点 应用场景
AVL 树 严格平衡(平衡因子绝对值 ≤ 1) 查找极快,但旋转频繁 查找密集型场景
红黑树 近似平衡(黑高相同) 增删效率高,旋转次数少 TreeMap、Go 标准库、内核调度
Treap 随机优先级 + 堆性质 实现简单,期望平衡 竞赛编程
Splay 树 自调整(访问后旋转到根) 局部性好时性能优异 缓存场景

六、AVL 树:严格平衡的典范

AVL 树是最早的自平衡二叉查找树,它要求任意节点的左右子树高度差不超过 1

  • 优点:查找效率极高,时间复杂度稳定 O(log n)
  • 缺点:为了保持严格平衡,插入和删除时可能需要频繁旋转,维护成本高

AVL 树插入(Go 实现)

go 复制代码
func insertAVL(node *TreeNode, val int) *TreeNode {
    if node == nil {
        return &TreeNode{Val: val}
    }
    if val < node.Val {
        node.Left = insertAVL(node.Left, val)
    } else if val > node.Val {
        node.Right = insertAVL(node.Right, val)
    } else {
        return node // 重复值不插入
    }

    balance := getBalance(node)

    // LL 型:左子树的左子树过高
    if balance > 1 && val < node.Left.Val {
        return rightRotate(node)
    }
    // RR 型:右子树的右子树过高
    if balance < -1 && val > node.Right.Val {
        return leftRotate(node)
    }
    // LR 型:左子树的右子树过高
    if balance > 1 && val > node.Left.Val {
        node.Left = leftRotate(node.Left)
        return rightRotate(node)
    }
    // RL 型:右子树的左子树过高
    if balance < -1 && val < node.Right.Val {
        node.Right = rightRotate(node.Right)
        return leftRotate(node)
    }

    return node
}

七、红黑树:近似平衡的工程之选

红黑树是 BBST 家族中最著名的成员之一。它放弃了「严格平衡」,转而追求「近似平衡」,通过给节点增加颜色属性(红或黑),并遵守以下五条规则:

  1. 每个节点要么是红色,要么是黑色
  2. 根节点是黑色
  3. 所有叶子节点(NIL)都是黑色
  4. 红色节点的两个子节点必须是黑色(不能有两个连续的红色节点)
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

为什么工程中都用红黑树?

红黑树通过牺牲严格的平衡性,换取了更少的旋转次数。

特性 AVL 树 红黑树
平衡度 严格平衡 近似平衡
查找 O(log n),略快 O(log n),最多多一层
插入旋转 最多 2 次 最多 2 次
删除旋转 最坏 O(log n) 次 最多 3 次
实现复杂度 较高 相对简单

核心洞察 :红黑树的任何不平衡都可以在 三次旋转之内 解决!这使得它在频繁插入、删除的场景下表现更优。

正是因为这种「工程上的性价比」,Go 标准库中的内部实现、Java 的 TreeMapTreeSet,C++ 的 std::map,Linux 内核的完全公平调度器(CFS)都选择了红黑树。


八、B+ 树:数据库索引的幕后英雄

8.1 从二叉到多叉:磁盘 I/O 的考量

当数据量大到无法全部放入内存时,树节点需要存储在磁盘上。此时,树的高度直接决定了磁盘 I/O 的次数

二叉树的问题在于:每个节点最多只有两个子节点,导致树很高。假设有 1000 万条数据,二叉树高度约为 24,最坏情况下需要 24 次磁盘 I/O!

B 树(B-Tree)的解决思路很直接:

让每个节点存储多个关键字,拥有多个子节点(多叉树),从而降低树的高度。

8.2 B+ 树 vs B 树

MySQL 的 InnoDB 引擎使用的是 B+ 树 作为索引结构。B+ 树是 B 树的变种,主要有以下区别:

特性 B 树 B+ 树
数据存储 每个节点都存储数据 只有叶子节点存储数据,非叶子节点只存索引
叶子节点 相互独立 通过链表相连,支持范围查询
查询稳定性 可能在非叶子节点命中,不稳定 必须查到叶子节点,稳定 O(log n)
磁盘 I/O 节点大,一次读入的索引少 节点小,一次读入的索引多

为什么数据库偏爱 B+ 树?

  1. I/O 次数更少:非叶子节点不存数据,单个节点更小,一次磁盘页(通常 4KB)能装入更多索引
  2. 范围查询极快:叶子节点形成有序链表,范围查找只需遍历链表,无需回溯
  3. 查询性能稳定:每次查询都到叶子节点,不会出现「有时快有时慢」的情况

九、递归树:分析算法复杂度的利器

除了作为数据结构,树还可以用来分析算法的时间复杂度

一个非常实用的技巧是:递归树法

以归并排序为例:

scss 复制代码
            T(n)
           /    \
        T(n/2)  T(n/2)
        /  \     /  \
     T(n/4)... T(n/4)...

每一层的工作量都是 O(n) ,树的高度是 log n ,所以总时间复杂度就是 O(n log n)

递归树法特别适用于分析分治算法、递归算法的复杂度,把抽象的递归过程可视化,非常直观。


十、总结:一张图看懂树的演化

css 复制代码
┌─────────────────────────────────────────────────────────────┐
│                        树的演化之路                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   树(Tree)                                                 │
│     │                                                       │
│     ├── 二叉树(Binary Tree)                                │
│     │     │                                                 │
│     │     ├── 完全二叉树 → 堆(优先队列、Top K)              │
│     │     │                                                 │
│     │     └── 二叉查找树(BST)                              │
│     │           │                                           │
│     │           ├── 不平衡 → 退化成链表(O(n))              │
│     │           │                                           │
│     │           └── 平衡化 → BBST(平衡二叉查找树)           │
│     │                 │                                     │
│     │                 ├── AVL 树(严格平衡,查找快)          │
│     │                 │                                     │
│     │                 ├── 红黑树(近似平衡,增删快)          │
│     │                 │      └─ Go 标准库、Linux CFS        │
│     │                 │                                     │
│     │                 ├── Treap(随机平衡)                   │
│     │                 └── Splay 树(自调整)                  │
│     │                                                       │
│     └── 多叉树(Multi-way Tree)                             │
│           │                                                 │
│           └── B 树族                                         │
│                 ├── B 树(文件系统)                          │
│                 └── B+ 树(数据库索引,MySQL InnoDB)         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
数据结构 核心优势 典型应用
二叉查找树 有序存储、支持范围查询 基础理解
BBST 自动维持平衡,避免退化 通用平衡方案
AVL 树 查找极快 查找密集型场景
红黑树 增删效率高、实现相对简单 Go 标准库、内核调度
B+ 树 降低磁盘 I/O、范围查询快 MySQL 索引、文件系统
快速获取极值 Top K、优先队列

十一、写在最后

学习数据结构与算法的关键,不在于死记硬背,而在于掌握其中的思想和精髓,学会解决实际问题的方法。

树的演化过程,本质上就是计算机科学家们在不同约束条件下做权衡的过程:

  • BST 简单但会退化 → 引入 BBST 解决
  • BBST 中 AVL 树严格平衡但旋转多 → 红黑树牺牲一点平衡换更少旋转
  • 二叉树节点少、树太高 → B+ 树用多叉降低高度,减少磁盘 I/O

理解这些设计背后的为什么,比记住代码实现更重要。希望这篇文章能帮你建立起对「树」的系统认知。


如果这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬。你的支持是我持续输出的动力!


文章标签建议Go 算法 数据结构 二叉树 BBST 红黑树 B+树 MySQL 后端

相关推荐
渡我白衣1 小时前
并查集:基础认识与模拟实现
android·java·javascript·数据结构·c++·算法·并查集
hetao17338371 小时前
2026-09-01~09-04 hetao1733837 的刷题记录
c++·算法
Evand J1 小时前
【MATLAB例程,图像滤波5】 反谐波均值滑动窗口滤波(CHMF)图像降噪与质量评价,附代码下载链接
图像处理·算法·计算机视觉·matlab·均值算法·滑动窗口滤波·均值滑动
血小板要健康2 小时前
链表 阶段算法总结
java·数据结构·笔记·算法·leetcode·链表
江湖十年2 小时前
在 Go 中使用 dyno 包处理动态对象
后端·面试·go
a187927218312 小时前
【算法】回溯算法(三):三记重锤与 N 皇后——记忆化、状态设计与三层漏斗
算法·leetcode·go·剪枝·回溯·n皇后·算法讲解
纪卓志George3 小时前
打破语言范式:在 Go 里用动态代理实现 AOP
架构·go
HugoStudio_SWAN4 小时前
洛谷 P1420 / P1179 / B4262 最长连号、数字统计与词频统计——统计的三种面孔
c++·学习·程序人生·算法
青 春 记 忆5 小时前
LeetCode 350. 两个数组的交集 II|Python 解法详解
python·算法·leetcode