本文整理一般二叉树、二叉搜索树(BST)与平衡树的概念;BST 的插入、查找、删除与中序遍历的 Go 实现见我的 leetcode 仓库 下的 tree/(bst.go、bst_test.go)。
二叉树 (Binary Tree)
二叉树 (Binary Tree) 是包含 n 个节点的有限集合,该集合或者为空集(此时,二叉树称为空树),或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树的二叉树组成。
二叉树中的节点至多包含两棵子树,分别称为左子树和右子树,而左子树和右子树又分别至多包含两棵子树。由上述的定义,二叉树的定义是一种递归的定义。
graph TD
n1((1))
n2((2))
n3((3))
n4((4))
n5((5))
n7((7))
n10((10))
n11((11))
n14((14))
n1 --> n2
n1 --> n3
n2 --> n4
n2 --> n5
n3 --> n7
n5 --> n10
n5 --> n11
n7 --> n14
满二叉树 Full Binary Tree
对于一棵二叉树,如果每一个非叶子节点都存在左右子树,并且二叉树中所有的叶子节点都在同一层中,这样的二叉树称为满二叉树。
graph TD
n1((1))
n2((2))
n3((3))
n4((4))
n5((5))
n6((6))
n7((7))
n8((8))
n9((9))
n10((10))
n11((11))
n12((12))
n13((13))
n14((14))
n15((15))
n1 --> n2
n1 --> n3
n2 --> n4
n2 --> n5
n3 --> n6
n3 --> n7
n4 --> n8
n4 --> n9
n5 --> n10
n5 --> n11
n6 --> n12
n6 --> n13
n7 --> n14
n7 --> n15
完全二叉树 Complete Binary Tree
对于一棵具有 n 个节点的二叉树按照层次编号,同时,左右子树按照先左后右编号,如果编号为 i 的节点与同样深度的满二叉树中编号为 i 的节点在二叉树中的位置完全相同,则这棵二叉树称为完全二叉树。
graph TD
n1((1))
n2((2))
n3((3))
n4((4))
n5((5))
n6((6))
n7((7))
n8((8))
n9((9))
n10((10))
n11((11))
n1 --> n2
n1 --> n3
n2 --> n4
n2 --> n5
n3 --> n6
n3 --> n7
n4 --> n8
n4 --> n9
n5 --> n10
n5 --> n11
二叉搜索树 Binary Search Tree
BST(Binary Search Tree)是「二叉搜索树」,不是「二叉树」的简称。 也可叫二叉查找树、二叉排序树。一般二叉树只要求每个节点最多两个孩子;BST 在此基础上增加有序约束,用于相对高效的查找、插入与删除。BST 只保证有序,不保证平衡,名字前面不必加「平衡」。按有序序列插入会退化成链表;AVL、红黑树才是在 BST 之上再加平衡约束的自平衡二叉搜索树。
| 概念 | 含义 |
|---|---|
| 二叉树(Binary Tree) | 每个节点最多两个子节点,没有额外排序规则 |
| 二叉搜索树(BST) | 左小右大(递归),有序但不保证平衡 |
| 自平衡二叉搜索树 | BST + 平衡约束,如 AVL、红黑树 |
二叉树的一个重要用途是提高查找效率。HashMap 在哈希冲突严重、拉链过长时,会把链表升级为红黑树;红黑树本质上也是一棵自平衡的二叉搜索树。
二分查找能在有序数组上做到 O(log n),但动态插入、删除时维护有序数组成本很高。二叉搜索树(又叫二叉排序树)把「有序」这层约束放进树结构里:左子树存更小的值,右子树存更大的值,查找时可以像二分一样逐层缩小范围。
定义
一棵二叉搜索树满足以下三条性质(递归定义):
- 若左子树不空,则左子树上所有节点的值均小于根节点的值
- 若右子树不空,则右子树上所有节点的值均大于或等于根节点的值
- 左、右子树也分别是二叉搜索树
下图 (a) 是一棵 4 层的二叉搜索树。以根节点 8 为例:左子树(以 4 为根)和右子树(以 12 为根)各自都是一棵完整的 BST;再看中间的 4,它的左子树(以 2 为根)和右子树(以 6 为根)同样满足递归定义。每个非叶子节点都能拆成「左 BST + 根 + 右 BST」三部分。
graph TD
n8((8))
n4((4))
n12((12))
n2((2))
n6((6))
n10((10))
n14((14))
n1((1))
n3((3))
n5((5))
n7((7))
n9((9))
n11((11))
n13((13))
n15((15))
n8 --> n4
n8 --> n12
n4 --> n2
n4 --> n6
n12 --> n10
n12 --> n14
n2 --> n1
n2 --> n3
n6 --> n5
n6 --> n7
n10 --> n9
n10 --> n11
n14 --> n13
n14 --> n15
节点表示(LeetCode / Go)
刷题与工程里常见如下节点定义(与 LeetCode TreeNode 一致):
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
| 字段 | 作用 |
|---|---|
Val |
节点保存的数据 |
Left / Right |
指向左右子节点的指针,本身不存业务数据 |
插入、查找、删除时,用待处理的值与 node.Val 比较,决定走左还是走右。链式存储的 C 写法里常把数据域写作 data,语义相同。
实现参考
tree/bst.go 提供 Insert、Search、Delete、InOrder 等 API,本地可运行:
go test -v ./tree/...
(在 leetcode 仓库 根目录执行。)
性质与中序遍历
中序遍历(左 → 根 → 右)一棵二叉搜索树,得到的序列一定是有序的。例如上图 (a) 的中序遍历结果为:1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15。
中序遍历也是验证 BST 是否正确的常用手段:结果须严格(或按约定)递增。遍历整棵树的时间复杂度为 O(n),除结果切片外递归栈约为 O(h)(h 为树高)。
查找性能
| 操作 | 平均(树较平衡) | 最坏(退化成链) |
|---|---|---|
| 查找 / 插入 / 删除 | O(log n) | O(n) |
| 情况 | 时间复杂度 | 说明 |
|---|---|---|
| 最好 | O(log n) | 树大致平衡,每层排除约一半节点,接近折半查找 |
| 最坏 | O(n) | 树退化为链表,需遍历全部节点 |
最坏情况常见于按有序序列依次插入:每次新节点都落到同一侧,整棵树变成一条链。下图 (b) 是按 1 → 2 → 3 → 4 → 5 顺序插入后的结果。
graph TD
n1((1))
n2((2))
n3((3))
n4((4))
n5((5))
n1 --> n2
n2 --> n3
n3 --> n4
n4 --> n5
正因为存在这种退化,实际工程中很少直接使用普通二叉搜索树,而是引入 AVL 树、红黑树等自平衡结构(见下文)。
插入与查找
- 插入:从根出发,值小于当前节点则进左子树,大于则进右子树,直到空位新建节点。
- 查找:同样从根比较
Val,每次排除一侧子树。
思路与有序数组上的二分查找类似,但不需要整体搬移元素。
删除节点
删除要区分三种情况(deleteNode):
- 无左子:用右子顶替当前节点(改指针,返回
node.Right)。 - 无右子:用左子顶替(返回
node.Left)。 - 左右都有:不是在原位置硬删后把右子树整体上移,而是:
- 在右子树中找最小值(一直沿
Left到底)→ 中序后继(in-order successor) - 将该值复制到当前节点的
Val(节点位置不变) - 在右子树里再删除这个后继(它至多只有右子,属于前两种情况)
- 在右子树中找最小值(一直沿
删除值为 5 的示例(右子树最小值为 6):
graph TD
subgraph before["删除前"]
b5((5))
b3((3))
b7((7))
b6((6))
b8((8))
b5 --> b3
b5 --> b7
b7 --> b6
b7 --> b8
end
graph TD
subgraph after["删除后:Val 用 6 覆盖,再删原 6"]
a6((6))
a3((3))
a7((7))
a8((8))
a6 --> a3
a6 --> a7
a7 --> a8
end
等价做法是用左子树最大值(中序前驱)替换;leetcode 实现 选用右子树最小值,是常见写法。
平衡二叉树 Balanced Binary Tree
上一节图 (b) 的退化链表,查找退化为 O(n)。平衡二叉树通过限制左右子树的高度差,让树在插入、删除后仍保持大致平衡,把查找维持在 O(log n) 量级。
工程里常说的「平衡二叉树」通常指同时满足 BST 性质与平衡条件的树,例如 AVL 树、红黑树。本节先说明平衡条件本身,再用一个插入示例看「失衡 → 旋转 → 恢复平衡」。
定义
一棵平衡二叉树满足(递归定义):
- 空树是平衡二叉树
- 非空树左右子树高度之差的绝对值不超过 1
- 左右子树也分别是平衡二叉树
常用平衡因子表示:平衡因子 = 左子树高度 − 右子树高度,合法取值为 -1、0、1。绝对值大于 1 的节点即为失衡点,需要通过旋转调整。
插入示例:从平衡到失衡再到旋转
下面用同一组插入顺序演示(同时保持 BST 性质:左小右大)。
步骤 1:插入 12,再插入 24。根 12 的左子树高度 0、右子树高度 1,高度差为 1,仍平衡。
graph TD
n12((12))
n24((24))
n12 --> n24
步骤 2:再插入 28,落到 24 的右侧(右子树的右子树,简称右右 / RR)。此时根 12 的左子树高度 0、右子树高度 2,高度差为 2,失衡。
graph TD
n12((12))
n24((24))
n28((28))
n12 --> n24
n24 --> n28
步骤 3:在失衡点 12 处做一次左旋(把 24 提为根,12 成为其左孩子,28 挂在 24 右侧)。旋转后根 24 的左右子树高度均为 1,恢复平衡,BST 性质不变。
graph TD
n24((24))
n12((12))
n28((28))
n24 --> n12
n24 --> n28
RR 只需一次单旋即可。其它三种失衡(左左 LL、左右 LR、右左 RL)也是先判断插入落在哪一侧,再选择单旋或先旋子树再旋根的「双旋」。AVL 树把这四种情况固化在插入、删除逻辑里。
小结
| 概念 | 说明 |
|---|---|
| 平衡条件 | 任意节点左右子树高度差 ≤ 1 |
| 失衡原因 | 连续向同一侧重插入或删除 |
| 恢复手段 | 左旋 / 右旋(必要时双旋) |
| 与 BST 关系 | 通常叠加 BST 有序性,才用于查找 |
严格维护高度平衡的 AVL 树旋转次数较多;红黑树放宽平衡条件,换取更少的旋转,在 HashMap、Linux 内核等场景更常见。
自平衡二叉搜索树
BST 只保证有序,不保证平衡。某棵普通 BST 碰巧很矮,仍然只是 BST:下次按有序序列插入,还是会退化成链。要维持 O(log n),必须在每次插入、删除后自动恢复平衡。
自平衡二叉搜索树是一个家族名,不是某一种具体实现。完整名字是「二叉搜索树」;只有这一层才在前面加「自平衡」或「平衡」。AVL 树、红黑树都属于这个家族:有序性来自 BST,区别只在如何恢复平衡。
二叉树
└─ 二叉搜索树(有序,不保证平衡)
└─ 自平衡二叉搜索树(有序 + 插入删除后自动维持平衡)
├─ 红黑树(工程默认,颜色规则,近似平衡)
├─ AVL 树(最早,高度差 ≤ 1,严格平衡)
├─ 伸展树(访问时旋转到根)
└─ 树堆(随机优先级)
「平衡」在不同树里标准不一样:AVL 要求任意节点左右高度差不超过 1;红黑树只保证最长路径不超过最短路径的两倍。
| 结构 | 平衡方式 | 典型场景 |
|---|---|---|
| 红黑树 | 节点着色,最长路径 ≤ 2× 最短 | C++ map / set、Java TreeMap / TreeSet、HashMap 冲突桶、Linux CFS |
| AVL 树 | 平衡因子 ∈ {-1, 0, 1} | 读多写少的内存索引;查找比红黑树更稳,插入删除旋转可能更多 |
| 伸展树(Splay Tree) | 不额外记颜色或高度,访问时把节点旋到根 | 热点数据会自己浮上来,均摊 O(log n),单次可能很慢 |
| 树堆(Treap) | 每个节点一个 BST 键、一个堆优先级(通常随机) | 按键满足 BST、按优先级满足堆,期望高度 O(log n);竞赛和随机化算法里常见 |
日常写代码,记住红黑树和 AVL 即可。伸展树、树堆知道定位就够。AVL 的旋转与插入删除见 AVL Tree, AVL 树;红黑树的着色规则见 Red-Black Tree, 红黑树。
B 树 / B+ 树是平衡多路搜索树,不是二叉树。数据库索引、文件系统用它们,存储场景里比上面大多数二叉实现更重要。和 BST 家族的关系是「都是搜索树、都要平衡」,不是「自平衡 BST 的一种」。见 B-Tree, B+ Tree。
二叉树的一些性质
- 第 i 层上至多有 \(2^{i-1}\) 个节点(i ≥ 1)
- 深度为 k 的二叉树至多有 \(2^k - 1\) 个节点(k ≥ 1)
- 叶子节点数为 \(n_0\)、度为 2 的节点数为 \(n_2\) 时,有 \(n_0 = n_2 + 1\)
- 具有 n 个节点的完全二叉树深度为 \(\lfloor \log_2 n \rfloor + 1\)
存储结构
对二叉树操作前需选定存储方式:
- 顺序存储:用一维数组按层次编号存节点,下标体现逻辑关系;非完全二叉树会出现大量空位,空间浪费明显。
- 链式存储:每个节点含数据域与左右指针,适合一般二叉树与 BST。C 语言中常写作:
typedef struct BiNode {
int data;
struct BiNode *left;
struct BiNode *right;
} BiNode;
二叉树的遍历
遍历是二叉树上的基本操作,常见四种:
| 遍历 | 顺序 | 在 BST 上的典型结果 |
|---|---|---|
| 前序 Preorder | 根 → 左 → 右 | 一般无序 |
| 中序 Inorder | 左 → 根 → 右 | 升序 |
| 后序 Postorder | 左 → 右 → 根 | 一般无序 |
| 层次 | 按层从左到右 | 一般无序 |
InOrder 里的 In 表示根在左右子树之间(in between),是算法里的固定术语,不是日常口语里的「在里面」。LeetCode、教材与面试里 InOrder / inorderTraversal 非常常见;若更强调业务语义,也可命名 ValuesAscending,但读他人代码时认 InOrder 更省事。
下面用链式节点的前 / 中 / 后序递归示例(与上文 BiNode 对应):
// 先序:根 → 左 → 右
void pre_order(BiNode *p) {
if (p != NULL) {
printf("%d\t", p->data);
pre_order(p->left);
pre_order(p->right);
}
}
// 中序:左 → 根 → 右(BST 上为升序)
void in_order(BiNode *p) {
if (p != NULL) {
in_order(p->left);
printf("%d\t", p->data);
in_order(p->right);
}
}
// 后序:左 → 右 → 根
void post_order(BiNode *p) {
if (p != NULL) {
post_order(p->left);
post_order(p->right);
printf("%d\t", p->data);
}
}
层次遍历用队列按层展开,从根开始,每弹出一个节点就把其左右孩子入队:
// 层次遍历(伪代码思路:队列 + 逐层扩展)
void level_order(BiNode *p) {
// 使用队列,依次访问 front,并将 left/right 入队
}
Go 版 BST 的中序结果见 BST.InOrder。
参考
早期部分内容整理自:
维护记录
| 时间 | 修改内容 | 原因 |
|---|---|---|
| 2026-07-14 | 「AVL 与常见自平衡树」一节补充内链至新文章 AVL Tree, AVL 树 与 Red-Black Tree, 红黑树 | inbox/平衡二叉树.md 已整理为正式文章 avl-tree.md,与本文形成完整的平衡树系列,互相内链 |
| 2026-09-15 | 正文统一称「二叉搜索树」;补充 BST 默认不保证平衡、不必加「平衡」前缀 | 与 CLRS / 力扣 / 维基译名对齐;避免把普通 BST 和自平衡 BST 混为一谈 |
| 2026-10-09 | 修复本地 Hugo 构建的 Raw HTML 警告:代码/配置放入代码块,正文中的尖括号占位符改为行内代码 | 正文中的 HTML/XML 片段被当作原始 HTML 丢弃;文件名/URL/标题按规范调整 |