"树就像家族族谱,从祖先到后代,层层分明!" 👨👩👧👦
🌳 什么是树?家族族谱的故事
👨👩👧👦 生活中的树
爷爷(根节点)
/ \
爸爸 叔叔
/ \ \
我 弟弟 堂弟
术语:
- 根节点:爷爷(最上面的节点)
- 父节点:爸爸是我的父节点
- 子节点:我和弟弟是爸爸的子节点
- 兄弟节点:我和弟弟是兄弟
- 叶子节点:我、弟弟、堂弟(没有孩子的节点)
- 高度:从根到叶子的最长路径(这里是3层)
📊 二叉树(Binary Tree)
基本概念
二叉树:每个节点最多有2个子节点
1
/ \
2 3
/ \ \
4 5 6
节点定义
class TreeNode {
int val;
TreeNode left; // 左孩子
TreeNode right; // 右孩子
TreeNode(int val) {
this.val = val;
}
}
二叉树的类型
1. 满二叉树(Full Binary Tree)
每层都是满的!
1
/ \
2 3
/ \ / \
4 5 6 7
节点数:2^h - 1(h是高度)
2. 完全二叉树(Complete Binary Tree)
除了最后一层,其他层都是满的,
最后一层的节点靠左排列
1
/ \
2 3
/ \ /
4 5 6
用途:堆(Heap)就是完全二叉树!
3. 二叉搜索树(BST - Binary Search Tree)⭐⭐⭐
性质:
- 左子树的所有节点 < 根节点
- 右子树的所有节点 > 根节点
- 左右子树也是BST
5
/ \
3 7
/ \ / \
2 4 6 8
查找、插入、删除:平均O(log n)
最坏情况:退化成链表O(n)
BST操作:
// 查找
TreeNode search(TreeNode root, int val) {
if (root == null || root.val == val) {
return root;
}
if (val < root.val) {
return search(root.left, val); // 去左子树找
} else {
return search(root.right, val); // 去右子树找
}
}
// 插入
TreeNode insert(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
}
if (val < root.val) {
root.left = insert(root.left, val);
} else {
root.right = insert(root.right, val);
}
return root;
}
⚖️ AVL树(平衡二叉搜索树)
为什么需要AVL树?
BST的问题:
插入顺序:1, 2, 3, 4, 5
BST退化成链表:
1
\
2
\
3
\
4
\
5
查找效率:O(n) 😱
AVL树的解决方案:自动平衡!
AVL树会自动旋转,保持平衡:
3
/ \
2 4
/ \
1 5
查找效率:O(log n) ✅
AVL树的平衡条件
平衡因子 = 左子树高度 - 右子树高度
每个节点的平衡因子 ∈ {-1, 0, 1}
如果超出范围,需要旋转!
四种旋转
1. LL(左左)- 右旋
2. RR(右右)- 左旋
3. LR(左右)- 先左旋后右旋
4. RL(右左)- 先右旋后左旋
例子:LL情况
不平衡: 右旋后:
3 2
/ / \
2 1 3
/
1
平衡因子:3的平衡因子=2,超标!
解决:右旋
🔴⚫ 红黑树(Red-Black Tree)⭐⭐⭐⭐⭐
为什么Java用红黑树而不是AVL树?
| 特性 | AVL树 | 红黑树 |
|---|---|---|
| 平衡性 | 😊 严格平衡 | 😐 近似平衡 |
| 查找速度 | ⚡ 略快 | ⚡ 略慢 |
| 插入删除 | 🐌 慢(频繁旋转) | ⚡ 快 |
| 应用 | 查询多 | 插入删除多 |
结论:红黑树在插入删除操作更多的场景下更优!
红黑树的5条性质
1. 每个节点不是红色就是黑色 🔴⚫
2. 根节点是黑色 ⚫
3. 叶子节点(NIL)是黑色 ⚫
4. 红色节点的子节点必须是黑色(不能有连续的红色)🔴
5. 从任意节点到其叶子的所有路径,包含相同数量的黑色节点
红黑树示例
⚫7
/ \
🔴3 🔴18
/ \ / \
⚫1 ⚫5 ⚫10⚫22
红黑树的应用
✅ Java的TreeMap、TreeSet
✅ C++ STL的map、set
✅ Linux内核的进程调度
✅ HashMap的链表转红黑树(JDK 8+)
📚 B树(B-Tree)
为什么需要B树?
问题:二叉树在磁盘存储场景下效率低!
磁盘读取特点:
- 按块读取(每次读4KB或更多)
- 读取慢(机械硬盘)
二叉树问题:
- 每个节点只存1个数据,浪费!
- 树高太高,读取次数多
解决方案:B树 - 多叉树,每个节点存多个数据!
B树的特点
一个5阶B树:
[50, 100]
/ | \
[20,30] [60,70] [110,120]
/ | \ / | \ / | \
... ... ... ... ... ... ... ... ...
特点:
- 每个节点可以有多个key(不只2个)
- 每个节点可以有多个子节点
- 所有叶子节点在同一层
- 节点内的key有序
B树的定义
m阶B树:
1. 每个节点最多有m个子节点
2. 除根外,每个节点至少有⌈m/2⌉个子节点
3. 根节点至少有2个子节点(除非是叶子)
4. 所有叶子在同一层
5. 有k个子节点的节点包含k-1个key
🏆 B+树(B+ Tree)⭐⭐⭐⭐⭐⭐
MySQL为什么用B+树做索引?
B树 vs B+树
| 特性 | B树 | B+树 |
|---|---|---|
| 数据存储 | 所有节点都存数据 | 只有叶子节点存数据 |
| 叶子链表 | 无 | 叶子节点形成链表 |
| 范围查询 | 🐌 慢 | ⚡ 超快 |
| 顺序访问 | 🐌 慢 | ⚡ 超快 |
B+树结构
B+树(MySQL InnoDB索引):
[50, 100]
/ | \
[20,30] [60,70] [110,120]
↓ ↓ ↓
[10,20,30] ⇄ [50,60,70] ⇄ [100,110,120] ⇄ ...
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
数据 数据 数据 数据 数据 数据 数据 数据 数据
特点:
1. 非叶子节点只存key,不存数据(索引)
2. 所有数据都在叶子节点
3. 叶子节点形成有序链表(⇄)
为什么B+树适合MySQL?
1. 范围查询超快!⚡
查询 age BETWEEN 20 AND 30:
B树:需要遍历整棵树 😱
B+树:找到20,然后沿着链表一直到30 ✅
[10,20,30] ⇄ [40,50,60] ⇄ [70,80,90]
↑____________↑
从20开始,到30结束
2. 磁盘IO次数少!💾
B+树的非叶子节点不存数据,只存key:
→ 每个节点能存更多key
→ 树的高度更低
→ 磁盘IO次数更少
例子:
B树:高度4,需要4次IO
B+树:高度3,需要3次IO ✅
3. 全表扫描高效!📊
B+树叶子节点形成链表:
遍历链表即可,不需要遍历整棵树!
[10] ⇄ [20] ⇄ [30] ⇄ [40] ⇄ [50] ...
MySQL InnoDB的B+树
一个节点 = 一页 = 16KB
假设:
- key: 8字节
- 指针: 6字节
- 一个节点可存: 16KB / 14B ≈ 1170个key
3层B+树能存多少数据?
第1层:1个节点
第2层:1170个节点
第3层:1170 × 1170 = 1,368,900个叶子节点
如果每个叶子节点存16条数据:
1,368,900 × 16 = 21,902,400条数据
结论:3层B+树能存2000万+数据!😱
查找只需3次IO!⚡
代码示例:B+树节点
class BPlusTreeNode {
boolean isLeaf; // 是否是叶子节点
int[] keys; // key数组
BPlusTreeNode[] children; // 子节点指针(非叶子节点)
Object[] data; // 数据(叶子节点)
BPlusTreeNode next; // 下一个叶子节点(叶子节点的链表)
BPlusTreeNode(boolean isLeaf) {
this.isLeaf = isLeaf;
this.keys = new int[MAX_KEYS];
if (isLeaf) {
this.data = new Object[MAX_KEYS];
} else {
this.children = new BPlusTreeNode[MAX_KEYS + 1];
}
}
}
🎯 二叉树的遍历
1. 前序遍历(Pre-order)
顺序:根 → 左 → 右
1
/ \
2 3
/ \
4 5
遍历顺序:1 → 2 → 4 → 5 → 3
void preorder(TreeNode root) {
if (root == null) return;
System.out.print(root.val + " "); // 根
preorder(root.left); // 左
preorder(root.right); // 右
}
2. 中序遍历(In-order)⭐
顺序:左 → 根 → 右
5
/ \
3 7
/ \ / \
2 4 6 8
遍历顺序:2 → 3 → 4 → 5 → 6 → 7 → 8
BST的中序遍历是有序的!✅
void inorder(TreeNode root) {
if (root == null) return;
inorder(root.left); // 左
System.out.print(root.val + " "); // 根
inorder(root.right); // 右
}
3. 后序遍历(Post-order)
顺序:左 → 右 → 根
1
/ \
2 3
/ \
4 5
遍历顺序:4 → 5 → 2 → 3 → 1
void postorder(TreeNode root) {
if (root == null) return;
postorder(root.left); // 左
postorder(root.right); // 右
System.out.print(root.val + " "); // 根
}
4. 层序遍历(Level-order)⭐
一层一层遍历:
1
/ \
2 3
/ \
4 5
遍历顺序:1 → 2 → 3 → 4 → 5
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.print(node.val + " ");
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
}
🏆 经典面试题
1. 验证二叉搜索树(LeetCode 98)
public boolean isValidBST(TreeNode root) {
return isValidBST(root, null, null);
}
private boolean isValidBST(TreeNode node, Integer lower, Integer upper) {
if (node == null) return true;
int val = node.val;
if (lower != null && val <= lower) return false;
if (upper != null && val >= upper) return false;
return isValidBST(node.left, lower, val) &&
isValidBST(node.right, val, upper);
}
2. 二叉树的最大深度(LeetCode 104)
public int maxDepth(TreeNode root) {
if (root == null) return 0;
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
3. 二叉树的最近公共祖先(LeetCode 236)
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) {
return root; // 一个在左,一个在右,root就是LCA
}
return left != null ? left : right;
}
📊 树形结构总结
| 树类型 | 特点 | 时间复杂度 | 应用场景 |
|---|---|---|---|
| 二叉树 | 每个节点≤2个子节点 | - | 基础 |
| BST | 左<根<右 | O(log n)~O(n) | 查找 |
| AVL树 | 严格平衡 | O(log n) | 查找密集 |
| 红黑树 | 近似平衡 | O(log n) | TreeMap |
| B树 | 多叉树 | O(log n) | 数据库 |
| B+树 | 叶子存数据+链表 | O(log n) | MySQL索引 |
📝 总结
🎓 记忆口诀
树像家族谱,层层有分明。
二叉树最基础,BST能查找。
AVL严格平衡,红黑树更灵活。
HashMap用红黑树,TreeMap也用它。
B树是多叉树,磁盘存储佳。
B+树MySQL用,范围查询快。
叶子节点成链表,顺序遍历妙!
恭喜你!🎉 你已经掌握了从二叉树到B+树的核心知识!
这些是Java高级工程师面试的重点中的重点!💪
📌 重点记忆:
- 红黑树:HashMap、TreeMap
- B+树:MySQL InnoDB索引
- BST中序遍历有序
🤔 思考题:为什么MySQL不用红黑树而用B+树?
(答案:B+树磁盘IO少、范围查询快、叶子链表顺序访问快!)