第1章:树的基础知识
1.1 树的定义与特点
1.1.1 什么是树?
定义
树(Tree)是一种非线性的数据结构,它由n(n≥0)个有限节点组成一个具有层次关系的集合。
特点:
- 每个节点有零个或多个子节点
- 没有父节点的节点称为根节点
- 每一个非根节点有且只有一个父节点
- 除了根节点外,每个子节点可以分为多个不相交的子树
生活中的例子
文件系统:
根目录/
├── 文件夹A/
│ ├── 文件1.txt
│ └── 文件2.txt
├── 文件夹B/
│ └── 文件3.txt
└── 文件4.txt
组织架构:
CEO
├── 技术总监
│ ├── 开发经理
│ └── 测试经理
├── 市场总监
│ └── 销售经理
└── 财务总监
1.1.2 树的特点
核心特性
1. 层次性
- 树具有明显的层次结构
- 从根节点到叶子节点形成路径
2. 唯一性
- 每个节点只有一个父节点(根节点除外)
- 从根节点到任意节点有且仅有一条路径
3. 递归性
- 树是递归定义的
- 每个子树本身也是一棵树
与线性结构的对比
| 特性 | 线性结构(数组、链表) | 树结构 |
|---|---|---|
| 数据关系 | 一对一 | 一对多 |
| 存储方式 | 顺序或链式 | 层次结构 |
| 查找效率 | O(n) | O(log n)(平衡树) |
| 应用场景 | 简单数据存储 | 层次关系、搜索 |
1.2 树的术语
1.2.1 基本术语
节点相关
节点(Node): 树中的基本单位,包含数据和指向子节点的指针
根节点(Root): 树的最顶层节点,没有父节点
叶子节点(Leaf): 没有子节点的节点,也称为终端节点
内部节点(Internal Node): 有子节点的节点
关系相关
父节点(Parent): 一个节点的直接上级节点
子节点(Child): 一个节点的直接下级节点
兄弟节点(Sibling): 具有相同父节点的节点
祖先节点(Ancestor): 从根节点到该节点的路径上的所有节点
后代节点(Descendant): 以该节点为根的子树中的所有节点
路径相关
路径(Path): 从根节点到某个节点的节点序列
路径长度(Path Length): 路径上边的数量
深度(Depth): 从根节点到该节点的路径长度
高度(Height): 从该节点到最远叶子节点的路径长度
树的高度: 根节点的高度
1.2.2 术语示例
示例树
A (根节点)
/ \
B C
/ \ \
D E F
术语说明:
- 根节点: A
- 叶子节点: D、E、F
- 内部节点: A、B、C
- 父节点: A是B和C的父节点
- 子节点: B和C是A的子节点
- 兄弟节点: B和C是兄弟节点
- 深度: A的深度为0,B的深度为1,D的深度为2
- 高度: D的高度为0,B的高度为1,A的高度为2
1.3 树的分类
1.3.1 按子节点数量分类
一般树
特点:
- 每个节点可以有任意数量的子节点
- 没有限制
示例:
A
/ | \
B C D
/ \ | / \
E F G H I
二叉树
特点:
- 每个节点最多有两个子节点
- 左子节点和右子节点
示例:
A
/ \
B C
/ \ \
D E F
1.3.2 按结构分类
完全二叉树
特点:
- 除了最后一层,其他层都是满的
- 最后一层从左到右填充
示例:
A
/ \
B C
/ \ / \
D E F G
满二叉树
特点:
- 所有层都是满的
- 每个节点要么是叶子节点,要么有两个子节点
示例:
A
/ \
B C
/ \ / \
D E F G
平衡二叉树
特点:
- 任意节点的左右子树高度差不超过1
- 保证查找性能
示例:
A
/ \
B C
/ \
D E
1.3.3 按应用分类
二叉搜索树(BST)
特点:
- 左子树所有节点值 < 根节点值
- 右子树所有节点值 > 根节点值
- 中序遍历是有序的
红黑树
特点:
- 自平衡二叉搜索树
- 保证最坏情况下的性能
AVL树
特点:
- 严格平衡的二叉搜索树
- 任意节点左右子树高度差不超过1
📊 本章总结
核心要点:
- 树是一种非线性数据结构,具有层次性
- 树的基本术语:节点、根、叶子、深度、高度等
- 树的分类:二叉树、完全二叉树、平衡二叉树等
- 树结构适合表示层次关系和高效搜索
第2章:二叉树深度剖析
2.1 二叉树基础
2.1.1 二叉树的定义
定义
**二叉树(Binary Tree)**是每个节点最多有两个子节点的树结构。
特点:
- 每个节点最多有两个子节点
- 左子节点和右子节点
- 可以为空(空树)
数学性质
1. 第i层最多有2^(i-1)个节点
2. 深度为k的二叉树最多有2^k - 1个节点
3. 对于任意二叉树,叶子节点数 = 度为2的节点数 + 1
2.1.2 二叉树的存储
顺序存储
使用数组存储:
// 完全二叉树可以用数组存储
// 索引关系:
// 父节点索引:i
// 左子节点索引:2*i + 1
// 右子节点索引:2*i + 2
示例:
树结构:
A(0)
/ \
B(1) C(2)
/ \ /
D(3) E(4) F(5)
数组:[A, B, C, D, E, F]
索引: 0 1 2 3 4 5
优势:
- 内存连续,CPU缓存友好
- 索引计算快速
劣势:
- 不适合非完全二叉树(浪费空间)
链式存储
使用节点存储:
class TreeNode {
int val;
TreeNode left; // 左子节点
TreeNode right; // 右子节点
TreeNode(int val) {
this.val = val;
}
}
优势:
- 灵活,适合任意二叉树
- 不浪费空间
劣势:
- 内存不连续
- 需要额外指针空间
2.2 二叉树的遍历
2.2.1 深度优先遍历(DFS)
前序遍历(Preorder)
顺序: 根 → 左 → 右
void preorder(TreeNode root) {
if (root == null) return;
System.out.println(root.val); // 访问根节点
preorder(root.left); // 遍历左子树
preorder(root.right); // 遍历右子树
}
示例:
树: 1
/ \
2 3
/ \
4 5
前序遍历:1 → 2 → 4 → 5 → 3
中序遍历(Inorder)
顺序: 左 → 根 → 右
void inorder(TreeNode root) {
if (root == null) return;
inorder(root.left); // 遍历左子树
System.out.println(root.val); // 访问根节点
inorder(root.right); // 遍历右子树
}
示例:
树: 1
/ \
2 3
/ \
4 5
中序遍历:4 → 2 → 5 → 1 → 3
后序遍历(Postorder)
顺序: 左 → 右 → 根
void postorder(TreeNode root) {
if (root == null) return;
postorder(root.left); // 遍历左子树
postorder(root.right); // 遍历右子树
System.out.println(root.val); // 访问根节点
}
示例:
树: 1
/ \
2 3
/ \
4 5
后序遍历:4 → 5 → 2 → 3 → 1
2.2.2 广度优先遍历(BFS)
层序遍历(Level Order)
顺序: 从上到下,从左到右
void levelOrder(TreeNode root) {
if (root == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
System.out.println(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
}
示例:
树: 1
/ \
2 3
/ \
4 5
层序遍历:1 → 2 → 3 → 4 → 5
2.3 二叉搜索树
2.3.1 定义与性质
定义
**二叉搜索树(Binary Search Tree,BST)**是一种特殊的二叉树:
性质:
- 左子树所有节点的值 < 根节点的值
- 右子树所有节点的值 > 根节点的值
- 左右子树都是二叉搜索树
示例
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
验证:
- 左子树(3, 1, 6, 4, 7)都 < 8
- 右子树(10, 14, 13)都 > 8
- 每个子树也满足BST性质
2.3.2 查找操作
实现原理
TreeNode search(TreeNode root, int key) {
if (root == null || root.val == key) {
return root;
}
if (key < root.val) {
return search(root.left, key); // 在左子树查找
} else {
return search(root.right, key); // 在右子树查找
}
}
时间复杂度:
- 最好情况:O(log n) - 平衡树
- 最坏情况:O(n) - 退化为链表
- 平均情况:O(log n)
2.3.3 插入操作
实现原理
TreeNode insert(TreeNode root, int key) {
if (root == null) {
return new TreeNode(key); // 创建新节点
}
if (key < root.val) {
root.left = insert(root.left, key); // 插入左子树
} else if (key > root.val) {
root.right = insert(root.right, key); // 插入右子树
}
return root;
}
时间复杂度: O(log n)平均,O(n)最坏
2.3.4 删除操作
三种情况
1. 删除叶子节点: 直接删除
2. 删除只有一个子节点的节点: 用子节点替换
3. 删除有两个子节点的节点: 用右子树的最小值替换
TreeNode delete(TreeNode root, int key) {
if (root == null) return null;
if (key < root.val) {
root.left = delete(root.left, key);
} else if (key > root.val) {
root.right = delete(root.right, key);
} else {
// 找到要删除的节点
if (root.left == null) return root.right;
if (root.right == null) return root.left;
// 有两个子节点:用右子树的最小值替换
TreeNode min = findMin(root.right);
root.val = min.val;
root.right = delete(root.right, min.val);
}
return root;
}
2.4 二叉树的退化问题
2.4.1 退化场景
问题描述
当二叉搜索树插入的元素有序时,会退化为链表:
示例:
插入顺序:1, 2, 3, 4, 5
退化后的树:
1
\
2
\
3
\
4
\
5
问题:
- 查找性能从O(log n)退化为O(n)
- 失去了二叉搜索树的优势
2.4.2 解决方案
自平衡二叉搜索树
解决方案: 使用自平衡二叉搜索树
常见实现:
- AVL树: 严格平衡,高度差不超过1
- 红黑树: 近似平衡,性能优秀
- B树/B+树: 多路平衡树
目标:
- 保证树的高度为O(log n)
- 保证查找、插入、删除的性能
📊 本章总结
核心要点:
- 二叉树是每个节点最多有两个子节点的树
- 二叉搜索树保证左小右大的性质
- 二叉搜索树在极端情况下会退化为链表
- 需要使用自平衡树来解决退化问题
第3章:平衡二叉树
3.1 平衡二叉树基础
3.1.1 定义
什么是平衡二叉树?
**平衡二叉树(Balanced Binary Tree)**是一种特殊的二叉搜索树,它保证树的高度平衡。
特点:
- 任意节点的左右子树高度差不超过1
- 保证查找性能为O(log n)
- 通过旋转操作保持平衡
3.1.2 平衡因子
定义
平衡因子(Balance Factor) = 左子树高度 - 右子树高度
平衡条件:
- 平衡因子的绝对值 <= 1
- 即:|左子树高度 - 右子树高度| <= 1
示例:
节点A:
左子树高度 = 2
右子树高度 = 1
平衡因子 = 2 - 1 = 1 ✓(平衡)
节点B:
左子树高度 = 3
右子树高度 = 1
平衡因子 = 3 - 1 = 2 ✗(不平衡)
3.2 AVL树
3.2.1 AVL树定义
特点
AVL树是最早发明的自平衡二叉搜索树:
性质:
- 是二叉搜索树
- 任意节点的左右子树高度差不超过1
- 通过旋转操作保持平衡
命名: 以发明者Adelson-Velsky和Landis的名字命名
3.2.2 AVL树的优势
严格平衡
优势:
- 严格平衡,查找性能稳定
- 最坏情况下也是O(log n)
- 适合查找频繁的场景
劣势:
- 插入和删除需要频繁旋转
- 维护成本高
3.3 旋转操作
3.3.1 左旋(Left Rotation)
操作过程
场景: 右子树过高,需要左旋
步骤:
- 将右子节点提升为新的根节点
- 原根节点成为新根节点的左子节点
- 新根节点原来的左子节点成为原根节点的右子节点
示意图:
左旋前:
A
\
B
/ \
C D
左旋后:
B
/ \
A D
\
C
代码实现
TreeNode leftRotate(TreeNode root) {
TreeNode newRoot = root.right;
root.right = newRoot.left;
newRoot.left = root;
return newRoot;
}
3.3.2 右旋(Right Rotation)
操作过程
场景: 左子树过高,需要右旋
步骤:
- 将左子节点提升为新的根节点
- 原根节点成为新根节点的右子节点
- 新根节点原来的右子节点成为原根节点的左子节点
示意图:
右旋前:
A
/
B
/ \
C D
右旋后:
B
/ \
C A
/
D
代码实现
TreeNode rightRotate(TreeNode root) {
TreeNode newRoot = root.left;
root.left = newRoot.right;
newRoot.right = root;
return newRoot;
}
3.3.3 左右旋和右左旋
左右旋(Left-Right Rotation)
场景: 左子树的右子树过高
步骤:
- 先对左子节点左旋
- 再对根节点右旋
右左旋(Right-Left Rotation)
场景: 右子树的左子树过高
步骤:
- 先对右子节点右旋
- 再对根节点左旋
📊 本章总结
核心要点:
- 平衡二叉树保证树的高度平衡
- AVL树是严格平衡的二叉搜索树
- 通过旋转操作保持平衡
- 旋转操作包括左旋、右旋、左右旋、右左旋
第4章:红黑树深度剖析
4.1 为什么使用红黑树?
4.1.1 二叉搜索树的退化问题
问题描述
二叉搜索树在极端情况下会退化为链表:
示例:
插入顺序:1, 2, 3, 4, 5, 6, 7
退化后的树:
1
\
2
\
3
\
4
\
5
\
6
\
7
问题:
- 查找性能从O(log n)退化为O(n)
- 失去了二叉搜索树的优势
4.1.2 解决方案
自平衡二叉搜索树
解决方案: 使用自平衡二叉搜索树
常见实现:
- AVL树: 严格平衡,但维护成本高
- 红黑树: 近似平衡,性能优秀(推荐)
- B树/B+树: 多路平衡树
红黑树的优势:
- 近似平衡,性能优秀
- 插入和删除的维护成本低于AVL树
- 适合频繁插入删除的场景
4.1.3 在HashMap中的应用
JDK8的优化
JDK7: HashMap使用数组+链表
JDK8: HashMap使用数组+链表/红黑树
优化原因:
- 当链表长度超过8时,转为红黑树
- 避免哈希冲突严重时性能退化
- 保证最坏情况下也是O(log n)
触发条件:
- 链表长度 >= 8
- 数组长度 >= 64
4.2 红黑树五大性质
4.2.1 性质详解
性质1:节点是红色或黑色
定义: 每个节点要么是红色,要么是黑色
作用: 通过颜色标记来维护平衡
性质2:根节点是黑色
定义: 根节点必须是黑色
作用: 保证树的稳定性
性质3:叶子节点(NIL)是黑色
定义: 所有叶子节点(NIL节点,空节点)都是黑色
注意: 红黑树中的叶子节点是指NIL节点,不是实际存储数据的节点
性质4:红节点的子节点必须是黑色
定义: 如果一个节点是红色,那么它的两个子节点都是黑色
作用: 保证不会出现连续的红节点
推论: 从根节点到任意叶子节点的路径上,红节点数量 <= 黑节点数量
性质5:从任一节点到叶子节点的黑节点数相同
定义: 从任意节点到其每个叶子节点的所有路径上,黑色节点的数量相同
作用: 这是红黑树平衡的关键性质
示例:
从根节点到叶子节点的路径:
路径1:黑 → 红 → 黑 → 黑(3个黑节点)
路径2:黑 → 黑 → 红 → 黑(3个黑节点)
路径3:黑 → 黑 → 黑(3个黑节点)
4.2.2 性质保证的平衡性
平衡性分析
从性质5可以推导:
- 从根节点到最远叶子节点的路径长度 <= 从根节点到最近叶子节点的路径长度的2倍
- 这保证了红黑树是近似平衡的
数学证明:
- 最短路径:全黑节点,长度为h
- 最长路径:黑红交替,长度为2h
- 最长路径 <= 2 * 最短路径
结论: 红黑树的高度 <= 2 * log(n+1)
4.3 红黑树平衡操作
4.3.1 左旋和右旋
左旋(Left Rotation)
操作过程:
左旋前:
A
\
B
/ \
C D
左旋后:
B
/ \
A D
\
C
代码实现:
TreeNode leftRotate(TreeNode root) {
TreeNode newRoot = root.right;
root.right = newRoot.left;
newRoot.left = root;
// 更新颜色
newRoot.color = root.color;
root.color = RED;
return newRoot;
}
右旋(Right Rotation)
操作过程:
右旋前:
A
/
B
/ \
C D
右旋后:
B
/ \
C A
/
D
代码实现:
TreeNode rightRotate(TreeNode root) {
TreeNode newRoot = root.left;
root.left = newRoot.right;
newRoot.right = root;
// 更新颜色
newRoot.color = root.color;
root.color = RED;
return newRoot;
}
4.3.2 插入后的调整
插入策略
基本步骤:
- 按照二叉搜索树的方式插入新节点
- 将新节点标记为红色
- 如果违反红黑树性质,进行调整
调整情况
情况1:父节点是黑色
- 不需要调整,直接插入
情况2:父节点是红色,叔叔节点是红色
- 将父节点和叔叔节点变黑
- 将祖父节点变红
- 继续向上调整
情况3:父节点是红色,叔叔节点是黑色(或不存在)
- 需要旋转调整
- 根据插入位置选择左旋或右旋
4.3.3 删除后的调整
删除策略
基本步骤:
- 按照二叉搜索树的方式删除节点
- 如果删除的是红色节点,不需要调整
- 如果删除的是黑色节点,需要调整
调整情况
情况1:替代节点是红色
- 将替代节点变黑,完成
情况2:替代节点是黑色,兄弟节点是红色
- 旋转调整
- 继续处理
情况3:替代节点是黑色,兄弟节点是黑色
- 根据兄弟节点的子节点情况调整
- 可能需要继续向上调整
4.4 红黑树 vs AVL树
4.4.1 对比分析
平衡性
| 特性 | AVL树 | 红黑树 |
|---|---|---|
| 平衡程度 | 严格平衡 | 近似平衡 |
| 高度差 | <= 1 | <= 2倍 |
| 查找性能 | 略好 | 略差 |
维护成本
| 特性 | AVL树 | 红黑树 |
|---|---|---|
| 插入旋转 | 可能多次 | 最多2次 |
| 删除旋转 | 可能多次 | 最多3次 |
| 维护成本 | 高 | 低 |
适用场景
AVL树:
- 查找频繁,插入删除较少
- 需要严格平衡
红黑树:
- 插入删除频繁
- 需要近似平衡即可
- HashMap、TreeMap使用红黑树
4.4.2 为什么HashMap选择红黑树?
原因分析
1. 性能平衡:
- 查找性能:O(log n),虽然略差于AVL树,但足够好
- 插入删除:维护成本低,性能更好
2. 实际场景:
- HashMap中插入删除频繁
- 红黑树的综合性能更好
3. 实现复杂度:
- 红黑树的实现相对简单
- 维护成本低
结论: 红黑树在综合性能上更适合HashMap的场景
📊 本章总结
核心要点:
- 红黑树是自平衡二叉搜索树,解决BST退化问题
- 红黑树有五大性质,保证近似平衡
- 通过旋转操作保持平衡
- 红黑树在综合性能上优于AVL树,适合HashMap
第5章:红黑树在Java中的应用
5.1 TreeMap中的红黑树
5.1.1 数据结构
节点定义
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left; // 左子节点
Entry<K,V> right; // 右子节点
Entry<K,V> parent; // 父节点
boolean color = BLACK; // 节点颜色
}
特点:
- 使用红黑树存储键值对
- 节点包含颜色信息
- 支持父节点指针,便于操作
5.1.2 插入操作
实现流程
TreeMap的put()方法:
- 按照二叉搜索树的方式查找插入位置
- 创建新节点,标记为红色
- 插入节点
- 调用fixAfterInsertion()调整红黑树
fixAfterInsertion()方法:
- 检查是否违反红黑树性质
- 根据情况旋转和变色
- 保证红黑树平衡
5.1.3 删除操作
实现流程
TreeMap的remove()方法:
- 按照二叉搜索树的方式查找节点
- 删除节点
- 调用fixAfterDeletion()调整红黑树
fixAfterDeletion()方法:
- 检查是否违反红黑树性质
- 根据情况旋转和变色
- 保证红黑树平衡
5.2 HashMap中的红黑树
5.2.1 转换条件
链表转红黑树
触发条件:
- 链表长度 >= 8
- 数组长度 >= 64
代码:
if (binCount >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, hash);
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); // 如果数组长度 < 64,先扩容
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 转换为红黑树
TreeNode<K,V> hd = null, tl = null;
// ... 转换逻辑
}
}
5.2.2 节点定义
TreeNode节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left; // 左子节点
TreeNode<K,V> right; // 右子节点
TreeNode<K,V> prev; // 前驱节点(用于链表)
boolean red; // 节点颜色
}
特点:
- 继承LinkedHashMap.Entry
- 包含红黑树需要的指针
- 保留prev指针,支持链表操作
5.2.3 红黑树转链表
转换条件
触发条件:
- 红黑树节点数 < 6
代码:
if (lc <= UNTREEIFY_THRESHOLD) {
untreeify(map); // 转换为链表
}
原因:
- 当节点数较少时,链表的性能已经足够好
- 减少维护红黑树的成本
5.3 性能分析
5.3.1 时间复杂度
各种操作
| 操作 | 链表 | 红黑树 |
|---|---|---|
| 查找 | O(n) | O(log n) |
| 插入 | O(1) | O(log n) |
| 删除 | O(n) | O(log n) |
实际性能
正常情况(链表长度 < 8):
- 使用链表,性能O(1)平均
冲突严重(链表长度 >= 8):
- 使用红黑树,性能O(log n)
- 避免退化为O(n)
5.3.2 空间复杂度
空间占用
链表:
- 每个节点:数据 + next指针
- 空间复杂度:O(n)
红黑树:
- 每个节点:数据 + left + right + parent + color
- 空间复杂度:O(n),但常数更大
权衡:
- 红黑树占用更多空间
- 但保证了性能
📊 本章总结
核心要点:
- TreeMap完全基于红黑树实现
- HashMap在链表长度>=8时转为红黑树
- 红黑树保证最坏情况下也是O(log n)
- 在冲突严重时,红黑树性能明显优于链表
第6章:红黑树高频面试题精选
6.1 树与二叉树面试题
面试题1:什么是树?树和线性结构有什么区别?
答案:
树的定义:
- 树是一种非线性数据结构
- 由n个节点组成,具有层次关系
- 每个节点有零个或多个子节点
与线性结构的区别:
| 特性 | 线性结构 | 树结构 |
|---|---|---|
| 数据关系 | 一对一 | 一对多 |
| 存储方式 | 顺序或链式 | 层次结构 |
| 查找效率 | O(n) | O(log n)(平衡树) |
| 应用场景 | 简单数据存储 | 层次关系、搜索 |
面试题2:什么是二叉树?二叉树有哪些性质?
答案:
二叉树定义:
- 每个节点最多有两个子节点的树
- 左子节点和右子节点
性质:
- 第i层最多有2^(i-1)个节点
- 深度为k的二叉树最多有2^k - 1个节点
- 对于任意二叉树,叶子节点数 = 度为2的节点数 + 1
面试题3:二叉树的遍历方式有哪些?它们有什么区别?
答案:
深度优先遍历(DFS):
- 前序遍历:根 → 左 → 右
- 中序遍历:左 → 根 → 右
- 后序遍历:左 → 右 → 根
广度优先遍历(BFS):
- 层序遍历:从上到下,从左到右
区别:
- DFS:使用递归或栈,适合深度搜索
- BFS:使用队列,适合层次遍历
面试题4:什么是二叉搜索树?它有什么特点?
答案:
二叉搜索树(BST)定义:
- 是特殊的二叉树
- 左子树所有节点值 < 根节点值
- 右子树所有节点值 > 根节点值
- 左右子树都是BST
特点:
- 中序遍历是有序的
- 查找、插入、删除的时间复杂度:O(log n)平均,O(n)最坏
面试题5:二叉搜索树在什么情况下会退化?如何解决?
答案:
退化情况:
- 插入有序数据时,会退化为链表
- 例如:插入1, 2, 3, 4, 5
退化后的树:
1
\
2
\
3
\
4
\
5
解决方案:
- 使用自平衡二叉搜索树
- 常见实现:AVL树、红黑树、B树
6.2 平衡二叉树面试题
面试题6:什么是平衡二叉树?平衡因子是什么?
答案:
平衡二叉树定义:
- 特殊的二叉搜索树
- 任意节点的左右子树高度差不超过1
平衡因子:
- 平衡因子 = 左子树高度 - 右子树高度
- 平衡条件:|平衡因子| <= 1
面试题7:什么是AVL树?它有什么特点?
答案:
AVL树定义:
- 最早发明的自平衡二叉搜索树
- 任意节点的左右子树高度差不超过1
特点:
- 严格平衡
- 查找性能稳定O(log n)
- 插入删除需要频繁旋转
- 维护成本高
面试题8:了解红黑树的左旋和右旋操作吗?它们是为了解决什么问题?
答案:
左旋操作:
- 场景:右子树过高
- 将右子节点提升为根节点
- 原根节点成为新根节点的左子节点
右旋操作:
- 场景:左子树过高
- 将左子节点提升为根节点
- 原根节点成为新根节点的右子节点
解决的问题:
- 调整树的结构,保持平衡
- 在插入和删除后恢复红黑树性质
6.3 红黑树核心面试题
面试题9:HashMap为什么在JDK8中引入红黑树?
答案:
原因:
- 解决性能退化: 当哈希冲突严重时,链表会变长,查找性能从O(1)退化为O(n)
- 保证性能下限: 红黑树保证最坏情况下也是O(log n)
- 实际场景: 当链表长度>=8时,转为红黑树,避免性能问题
优化效果:
- 正常情况:使用链表,O(1)平均
- 冲突严重:使用红黑树,O(log n)
- 避免退化为O(n)
面试题10:红黑树和AVL树(平衡二叉搜索树)有什么区别?为什么HashMap选择红黑树?
答案:
主要区别:
| 特性 | AVL树 | 红黑树 |
|---|---|---|
| 平衡程度 | 严格平衡 | 近似平衡 |
| 高度差 | <= 1 | <= 2倍 |
| 查找性能 | 略好 | 略差 |
| 插入旋转 | 可能多次 | 最多2次 |
| 删除旋转 | 可能多次 | 最多3次 |
| 维护成本 | 高 | 低 |
为什么选择红黑树:
- 综合性能更好: 虽然查找略差,但插入删除性能更好
- 维护成本低: 旋转次数少,维护成本低
- 适合场景: HashMap中插入删除频繁,红黑树更适合
面试题11:二叉搜索树在极端情况下会退化成链表,红黑树如何避免?
答案:
红黑树的避免机制:
-
五大性质保证:
- 性质5:从任一节点到叶子节点的黑节点数相同
- 这保证了树的高度 <= 2 * log(n+1)
-
自平衡机制:
- 插入和删除后自动调整
- 通过旋转和变色保持平衡
-
近似平衡:
- 虽然不是严格平衡,但保证了性能
- 最长路径 <= 2 * 最短路径
效果:
- 避免了退化为链表
- 保证查找性能为O(log n)
面试题12:简述红黑树的五个基本性质。
答案:
性质1:节点是红色或黑色
- 每个节点要么是红色,要么是黑色
性质2:根节点是黑色
- 根节点必须是黑色
性质3:叶子节点(NIL)是黑色
- 所有叶子节点(NIL节点)都是黑色
性质4:红节点的子节点必须是黑色
- 如果一个节点是红色,那么它的两个子节点都是黑色
- 保证不会出现连续的红节点
性质5:从任一节点到叶子节点的黑节点数相同
- 从任意节点到其每个叶子节点的所有路径上,黑色节点的数量相同
- 这是红黑树平衡的关键性质
面试题13:红黑树的插入和删除操作如何保持平衡?
答案:
插入操作:
- 按照BST方式插入,标记为红色
- 如果违反性质4(连续红节点),调整:
- 情况1:父节点是黑色,不需要调整
- 情况2:父节点和叔叔节点都是红色,变色
- 情况3:父节点是红色,叔叔节点是黑色,旋转
删除操作:
- 按照BST方式删除
- 如果删除的是黑色节点,调整:
- 情况1:替代节点是红色,变黑
- 情况2:兄弟节点是红色,旋转
- 情况3:兄弟节点是黑色,根据子节点调整
调整方式:
- 旋转:左旋、右旋
- 变色:改变节点颜色
面试题14:红黑树的时间复杂度是多少?
答案:
各种操作:
| 操作 | 时间复杂度 |
|---|---|
| 查找 | O(log n) |
| 插入 | O(log n) |
| 删除 | O(log n) |
原因:
- 红黑树的高度 <= 2 * log(n+1)
- 所有操作最多遍历树的高度
- 因此时间复杂度为O(log n)
面试题15:红黑树和普通二叉搜索树相比有什么优势?
答案:
优势:
- 避免退化: 不会退化为链表,保证性能
- 性能稳定: 最坏情况下也是O(log n)
- 维护成本低: 相比AVL树,维护成本更低
- 综合性能好: 在查找、插入、删除之间取得平衡
普通BST的问题:
- 可能退化为链表,性能O(n)
- 性能不稳定
6.4 红黑树应用面试题
面试题16:TreeMap是如何使用红黑树的?
答案:
实现方式:
- TreeMap完全基于红黑树实现
- 所有键值对存储在红黑树中
- 按键排序,支持范围查询
特点:
- 插入、删除、查找都是O(log n)
- 支持导航方法(ceilingKey、floorKey等)
- 线程不安全
面试题17:HashMap在什么情况下会使用红黑树?
答案:
转换条件:
- 链表长度 >= 8
- 数组长度 >= 64
转换过程:
- 检查链表长度
- 如果 >= 8,检查数组长度
- 如果数组长度 >= 64,转换为红黑树
- 否则,先扩容
转换回链表:
- 红黑树节点数 < 6时,转换回链表
面试题18:红黑树在HashMap中的性能提升有多大?
答案:
性能对比:
| 场景 | 链表 | 红黑树 |
|---|---|---|
| 正常情况 | O(1)平均 | O(log n) |
| 冲突严重 | O(n) | O(log n) |
提升效果:
- 正常情况:链表性能更好
- 冲突严重:红黑树避免O(n)退化
- 保证最坏情况下也是O(log n)
实际效果:
- 在冲突严重时,性能提升明显
- 避免了极端情况下的性能问题
面试题19:如何实现一个简单的红黑树?
答案:
基本结构:
class RBTree {
private static final boolean RED = true;
private static final boolean BLACK = false;
class Node {
int key;
Node left, right, parent;
boolean color;
}
// 左旋
Node leftRotate(Node root) {
Node newRoot = root.right;
root.right = newRoot.left;
newRoot.left = root;
newRoot.color = root.color;
root.color = RED;
return newRoot;
}
// 右旋
Node rightRotate(Node root) {
Node newRoot = root.left;
root.left = newRoot.right;
newRoot.right = root;
newRoot.color = root.color;
root.color = RED;
return newRoot;
}
// 插入
Node insert(Node root, int key) {
// 1. 按照BST方式插入
// 2. 标记为红色
// 3. 调整红黑树
return root;
}
}
面试题20:红黑树和B树、B+树有什么区别?
答案:
主要区别:
| 特性 | 红黑树 | B树 | B+树 |
|---|---|---|---|
| 节点子节点数 | 最多2个 | 多个 | 多个 |
| 应用场景 | 内存数据结构 | 磁盘存储 | 数据库索引 |
| 查找方式 | 二分查找 | 多路查找 | 多路查找 |
| 叶子节点 | 存储数据 | 存储数据 | 只存储索引 |
选择建议:
- 内存数据结构:红黑树
- 磁盘存储:B树或B+树
- 数据库索引:B+树