本文纲要
- 引言
- 树的基本结构:节点与二叉树
2.1 节点的构成
2.2 什么是二叉树
2.3 树的基本术语 - 二叉查找树(二叉排序树)
3.1 定义与特点
3.2 添加节点过程
3.3 代码演示:节点类与插入 - 平衡二叉树
4.1 为什么需要平衡
4.2 定义与判断 - 平衡二叉树的旋转机制
5.1 左旋(Left Rotation)
5.2 右旋(Right Rotation)
5.3 左左、左右、右右、右左四种情况 - 总结
引言
在 Java 集合框架中,TreeSet 是一种有序的 Set 实现。它的底层数据结构是红黑树(一种自平衡的二叉查找树)。要深入理解 TreeSet 的工作原理,首先要掌握树结构的基础知识。
本文将从最基础的二叉树讲起,逐步过渡到二叉查找树、平衡二叉树,并详细讲解平衡二叉树维持平衡的核心操作——左旋与右旋。这些内容是理解 TreeMap、TreeSet 底层实现的关键。
树的基本结构:节点与二叉树
1 ) 节点的构成
在树结构中,每一个元素称为一个节点(Node)。每一个节点通常包含四部分信息:
父节点的地址
自身存储的值
左子节点的地址
右子节点的地址
可以用如下 Java 类来表示:
public class TreeNode {
// 父节点
TreeNode parent;
// 存储的值
int value;
// 左子节点
TreeNode left;
// 右子节点
TreeNode right;
public TreeNode(int value) {
this.value = value;
}
}
一个节点如果没有父节点(即为根节点),则 parent 为 null;如果没有左子节点或右子节点,则相应的 left、right 为 null。
以下图为例:
A 的左子节点记录了 B 的地址,右子节点记录了 C 的地址;B 和 C 的父节点记录了 A 的地址。B 和 C 没有子节点,所以它们的左、右子节点均为 null。
2 ) 什么是二叉树
二叉树是指任意一个节点的子节点数量不超过 2 的树。子节点的数量在树结构中有一个专业术语叫做度(degree)。二叉树中每个节点的度 ≤ 2。
一个简单的二叉树示例:
3 ) 树的基本术语
根节点(Root Node):树的最顶层节点,没有父节点。
子节点(Child Node):某个节点的直接后代。
父节点(Parent Node):某个节点的直接前驱。
叶子节点(Leaf Node):度为零的节点,没有子节点。
树的高度(Height):树中节点的最大层数。
左子树 / 右子树:以根节点的左子节点为根的子树称为左子树,以右子节点为根的称为右子树。
以高度为 4 的二叉树为例:
- 第 1 层:节点 7
- 第 2 层:节点 4、10
- 第 3 层:节点 2、5、11
- 第 4 层:节点 12
树的高度 = 层数 = 4。
- 7 是根节点。
- 4 是 7 的左子节点,10 是 7 的右子节点。
- 蓝色虚线框住的部分(以 4 为根的子树)是根节点的左子树,高度为 3。
- 绿色虚线框住的部分(以 10 为根的子树)是根节点的右子树,高度为 3。
二叉查找树(二叉排序树)
1 ) 定义与特点
普通的二叉树在存储数据时没有任何规律,查找某个节点需要遍历整棵树,效率很低。
为了提高查找效率,引入了二叉查找树(Binary Search Tree,BST),也称为二叉排序树或二叉搜索树。
二叉查找树必须满足:
- 首先是一棵二叉树(每个节点最多两个子节点)。
- 对于任意节点,其左子节点的值 < 该节点的值。
- 对于任意节点,其右子节点的值 > 该节点的值。
简单理解:任意节点从左到右依次增大。
示例:
- 7 的左子节点 4 < 7,右子节点 10 > 7。
- 4 的左子节点 2 < 4,右子节点 5 > 4。
- 10 的左子节点 9 < 10,右子节点 11 > 10。
这是一棵标准的二叉查找树。
2 ) 添加节点过程
向二叉查找树中添加节点时,遵循小的存左边,大的存右边,相等的值不存的规则。
具体过程如下:
- 如果树为空,直接将新节点作为根节点。
- 如果树不为空,从根节点开始比较:
- 若新节点的值 < 当前节点的值,且当前节点的左子节点为空,则成为左子节点;否则进入左子树继续比较。
- 若新节点的值 > 当前节点的值,且当前节点的右子节点为空,则成为右子节点;否则进入右子树继续比较。
- 若新节点的值 == 当前节点的值,不存储(或根据具体需求处理)。
添加实例:依次添加 7、4、10、5。
- 添加 7:树空,7 成为根节点。
- 添加 4:4 < 7,且 7 的左子节点为空,所以 4 成为 7 的左子节点。
- 添加 10:10 > 7,且 7 的右子节点为空,所以 10 成为 7 的右子节点。
- 添加 5:5 < 7,向左走;5 > 4,且 4 的右子节点为空,所以 5 成为 4 的右子节点。
结果:
如果继续添加 12、13,会出现“一边倒”的情况,这在后面会引出平衡二叉树的需求。
3 ) 代码演示:节点类与插入
下面使用 Java 代码演示二叉查找树的节点结构和插入过程。
节点类(包含父节点引用,以备平衡树使用):
class TreeNode {
int value;
TreeNode parent;
TreeNode left;
TreeNode right;
public TreeNode(int value) {
this.value = value;
}
// 插入节点(二叉查找树规则)
public void insert(int newValue) {
if (newValue < this.value) {
if (left == null) {
left = new TreeNode(newValue);
left.parent = this;
} else {
left.insert(newValue);
}
} else if (newValue > this.value) {
if (right == null) {
right = new TreeNode(newValue);
right.parent = this;
} else {
right.insert(newValue);
}
}
// 相等则不处理
}
}
构建树并输出结构:
public class BSTDemo {
public static void main(String[] args) {
TreeNode root = new TreeNode(7);
root.insert(4);
root.insert(10);
root.insert(5);
// 输出树的结构(先序)
printTree(root, "");
}
public static void printTree(TreeNode node, String prefix) {
if (node == null) return;
System.out.println(prefix + "├─ " + node.value);
if (node.left != null || node.right != null) {
if (node.left != null) {
printTree(node.left, prefix + "│ ");
} else {
System.out.println(prefix + "│ ├─ null");
}
if (node.right != null) {
printTree(node.right, prefix + " ");
} else {
System.out.println(prefix + " ├─ null");
}
}
}
}
输出示例:
├─ 7
│ ├─ 4
│ │ ├─ null
│ │ ├─ 5
│ │ ├─ null
│ │ ├─ null
│ ├─ 10
│ ├─ null
│ ├─ null
平衡二叉树
1 ) 为什么需要平衡
二叉查找树的插入顺序会影响树的形状。例如按 7、10、11、12、13 的顺序依次插入:
整棵树退化成了一条链表。此时查找节点 13 需要比较 5 次,效率降低。树的高度差越大,查询效率就越接近 O(n)。为了避免这种情况,需要让树的左右子树高度尽可能接近,即平衡。
2 ) 定义与判断
平衡二叉树(AVL 树是最典型的一种)的定义:
- 二叉树中,左右子树的高度差不超过 1。
- 任意节点的左右子树也是平衡二叉树。
判断示例:
| 二叉树 | 是否平衡 | 原因 |
|---|---|---|
| 文本描述:根节点 7,左子树高度 3,右子树高度 4,但节点 10 的左子树高度 0,右子树高度 3,差超过 1 | 否 | 节点 10 左右高度差为 3 − 0 = 3 > 1 |
| 文本描述:节点 4 左子树高度 2,右子树高度 0 | 否 | 节点 4 左右高度差 2 − 0 = 2 > 1 |
| 文本描述:根节点 7,左子树高度 2,右子树高度 1;其他节点高度差均 ≤ 1 | 是 | 所有节点高度差都 ≤ 1 |
| 文本描述:根节点 7 左右高度均 2;节点 4 左高度 1,右高度 0,差 1 | 是 | 所有节点高度差 ≤ 1 |
平衡二叉树的查询效率始终保持在 O(log n) 级别,因为树的高度被控制在对数级别。
平衡二叉树的旋转机制
当向平衡二叉树中添加一个节点后,可能导致树不再平衡。此时,需要通过左旋和右旋操作来恢复平衡。
1 ) 左旋(Left Rotation)
触发条件:右子树过高(右侧节点偏多),需要将右侧节点向左旋转。
左旋步骤:
- 将原先的右子节点(设为 R)提升为新的根节点。
- 原先的根节点(设为 E)降级为 R 的左子节点。
- R 原先的左子节点移交给 E,作为 E 的右子节点。
简单左旋图解:
旋转前:
旋转后:
复杂左旋图解(R 节点有一个左子节点):
旋转前:
- 添加节点 12 后,右子树过高,触发左旋。
- 先忽略 9(10 的左子节点),将 10 提升为新根,7 降级为 10 的左子节点。
- 处理 9:原来 9 是 10 的左子节点,旋转后无法再作为 10 的左子节点(因为 7 已经是左子节点了),因此将 9 移交给降级后的 7,作为 7 的右子节点。
旋转后:
Java 代码演示左旋(假设节点包含父指针):
// 左旋操作,以当前节点为根进行左旋
public TreeNode rotateLeft() {
TreeNode newRoot = this.right; // 新根是当前的右子节点
if (newRoot == null) return this;
// 当前节点成为新根的左子节点
this.right = newRoot.left;
if (newRoot.left != null) {
newRoot.left.parent = this;
}
// 新根替换当前节点的位置
newRoot.parent = this.parent;
if (this.parent != null) {
if (this.parent.left == this) {
this.parent.left = newRoot;
} else {
this.parent.right = newRoot;
}
}
// 设置当前节点为新根的左子节点
newRoot.left = this;
this.parent = newRoot;
return newRoot; // 返回新的根节点
}
2 ) 右旋(Right Rotation)
触发条件:左子树过高,需要将左侧节点向右旋转。
右旋步骤:
- 将原先的左子节点(设为 L)提升为新的根节点。
- 原先的根节点(设为 E)降级为 L 的右子节点。
- L 原先的右子节点移交给 E,作为 E 的左子节点。
简单右旋图解:
旋转前:
旋转后:
复杂右旋图解(L 节点有一个右子节点 5):
旋转前:
- 添加节点 1 后,左子树过高,触发右旋。
- 先忽略 5(4 的右子节点),将 4 提升为新根,7 降级为 4 的右子节点。
- 处理 5:原来 5 是 4 的右子节点,旋转后交给降级后的 7,作为 7 的左子节点。
旋转后:
Java 代码演示右旋:
// 右旋操作,以当前节点为根进行右旋
public TreeNode rotateRight() {
TreeNode newRoot = this.left; // 新根是当前的左子节点
if (newRoot == null) return this;
// 当前节点成为新根的右子节点
this.left = newRoot.right;
if (newRoot.right != null) {
newRoot.right.parent = this;
}
// 新根替换当前节点的位置
newRoot.parent = this.parent;
if (this.parent != null) {
if (this.parent.left == this) {
this.parent.left = newRoot;
} else {
this.parent.right = newRoot;
}
}
// 设置当前节点为新根的右子节点
newRoot.right = this;
this.parent = newRoot;
return newRoot; // 返回新的根节点
}
3 ) 左左、左右、右右、右左四种情况
当插入节点导致失衡时,根据插入位置的不同,分为四种情况,分别采用不同的旋转组合来恢复平衡。
| 情况 | 插入位置 | 失衡特征 | 恢复操作 |
|---|---|---|---|
| 左左 (LL) | 根节点左子树的左子树 | 左子树高度 − 右子树高度 > 1,且左子节点的左子树高 | 一次右旋 |
| 左右 (LR) | 根节点左子树的右子树 | 左子树高度 − 右子树高度 > 1,且左子节点的右子树高 | 先局部左旋,再整体右旋 |
| 右右 (RR) | 根节点右子树的右子树 | 右子树高度 − 左子树高度 > 1,且右子节点的右子树高 | 一次左旋 |
| 右左 (RL) | 根节点右子树的左子树 | 右子树高度 − 左子树高度 > 1,且右子节点的左子树高 | 先局部右旋,再整体左旋 |
左左 (LL) 示例:
左右 (LR) 示例(在左子树的右子树插入 6):
失衡树:
- 第一步:对 4 的右子树进行左旋(局部),5 上提,4 下移。
- 第二步:得到类似“左左”的结构后,再对整棵树进行右旋。
旋转过程:
右右 (RR)、右左 (RL) 与左左、左右镜像对称,不再赘述。
判断与恢复平衡的伪代码:
// 插入节点后,从插入点向上回溯,检查每个节点的平衡因子
void rebalance(TreeNode node) {
while (node != null) {
// 计算平衡因子 = 左子树高度 - 右子树高度
int balance = getHeight(node.left) - getHeight(node.right);
if (balance > 1) { // 左子树过高
if (getHeight(node.left.left) >= getHeight(node.left.right)) {
// 左左情况 → 一次右旋
node = node.rotateRight();
} else {
// 左右情况 → 先左旋左子节点,再右旋当前节点
node.left = node.left.rotateLeft();
node = node.rotateRight();
}
} else if (balance < -1) { // 右子树过高
if (getHeight(node.right.right) >= getHeight(node.right.left)) {
// 右右情况 → 一次左旋
node = node.rotateLeft();
} else {
// 右左情况 → 先右旋右子节点,再左旋当前节点
node.right = node.right.rotateRight();
node = node.rotateLeft();
}
} else {
// 平衡,无需旋转
}
// 向上回溯
node = node.parent;
}
}
总结
本文从最基本的节点结构开始,循序渐进地介绍了二叉树、二叉查找树和平衡二叉树的概念与操作,并重点讲解了平衡二叉树通过左旋、右旋维持平衡的机制。
理解这些数据结构是学习 Java 中 TreeSet、TreeMap 底层红黑树的基础。红黑树是一种更复杂的自平衡二叉查找树,它同样依赖旋转操作来保持近似平衡,从而保证查找、插入、删除的最坏时间复杂度为 O(log n)。
联系红黑树的结构和规则,进一步完善对 TreeSet 底层实现的理解。
1059

被折叠的 条评论
为什么被折叠?



