refactor: rename files to Chinese and organize by category
Organized 50 interview questions into 12 categories: - 01-分布式系统 (9 files): 分布式事务, 分布式锁, 一致性哈希, CAP理论, etc. - 02-数据库 (2 files): MySQL索引优化, MyBatis核心原理 - 03-缓存 (5 files): Redis数据结构, 缓存问题, LRU算法, etc. - 04-消息队列 (1 file): RocketMQ/Kafka - 05-并发编程 (4 files): 线程池, 设计模式, 限流策略, etc. - 06-JVM (1 file): JVM和垃圾回收 - 07-系统设计 (8 files): 秒杀系统, 短链接, IM, Feed流, etc. - 08-算法与数据结构 (4 files): B+树, 红黑树, 跳表, 时间轮 - 09-网络与安全 (3 files): TCP/IP, 加密安全, 性能优化 - 10-中间件 (4 files): Spring Boot, Nacos, Dubbo, Nginx - 11-运维 (4 files): Kubernetes, CI/CD, Docker, 可观测性 - 12-面试技巧 (1 file): 面试技巧和职业规划 All files renamed to Chinese for better accessibility and organized into categorized folders for easier navigation. Generated with [Claude Code](https://claude.com/claude-code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
286
questions/08-算法与数据结构/B+树原理.md
Normal file
286
questions/08-算法与数据结构/B+树原理.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# B+ 树 (B+ Tree)
|
||||
|
||||
## 数据结构原理
|
||||
|
||||
### 什么是 B+ 树?
|
||||
B+ 树是一种自平衡的树数据结构,是 B 树的变体,专门为磁盘存储和索引设计。它在数据库系统和文件系统中广泛应用,特别适合磁盘等外部存储设备。
|
||||
|
||||
### B+ 树的特点
|
||||
|
||||
1. **多路搜索树**:每个节点可以有多个子节点
|
||||
2. **有序结构**:所有键值按顺序存储
|
||||
3. **平衡性**:从根到任何叶子的路径长度相同
|
||||
4. **叶节点链表**:所有叶子节点通过指针连接成有序链表
|
||||
|
||||
### B+ 树与 B 树的区别
|
||||
|
||||
| 特性 | B+ 树 | B 树 |
|
||||
|------|-------|------|
|
||||
| 叶子节点 | 包含所有键值和指针 | 只包含键值 |
|
||||
| 非叶子节点 | 只包含键值和指针,不包含数据 | 包含键值和指针,部分包含数据 |
|
||||
| 查找效率 | 查找路径相同,但范围查询更高效 | 查找路径稍长 |
|
||||
| 插入/删除 | 叶子节点统一操作,分布更均匀 | 数据分散在各级节点 |
|
||||
| 范围查询 | 直接遍历叶子链表,高效 | 需要中序遍历整个树 |
|
||||
|
||||
## 图解说明
|
||||
|
||||
```
|
||||
B+ 树结构示例(m=3):
|
||||
[10, 20, 30]
|
||||
/ | | \
|
||||
[5, 8, 10] [15,18,20] [25,28,30] []
|
||||
| | | |
|
||||
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]
|
||||
```
|
||||
|
||||
### 关键概念
|
||||
|
||||
- **阶(m)**:每个节点的最大子节点数
|
||||
- **键(Key)**:用于索引的值
|
||||
- **指针(Pointer)**:指向子节点或数据的地址
|
||||
- **叶子节点**:存储实际数据的节点
|
||||
- **内部节点**:用于索引的中间节点
|
||||
|
||||
## Java 代码实现
|
||||
|
||||
```java
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class BPlusTreeNode {
|
||||
List<Integer> keys;
|
||||
List<BPlusTreeNode> children;
|
||||
BPlusTreeNode next; // 叶子节点间的指针
|
||||
boolean isLeaf;
|
||||
|
||||
public BPlusTreeNode(boolean isLeaf) {
|
||||
this.keys = new ArrayList<>();
|
||||
this.children = new ArrayList<>();
|
||||
this.isLeaf = isLeaf;
|
||||
this.next = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class BPlusTree {
|
||||
private BPlusTreeNode root;
|
||||
private int order; // B+树的阶
|
||||
|
||||
public BPlusTree(int order) {
|
||||
this.root = new BPlusTreeNode(true);
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
// 插入操作
|
||||
public void insert(int key) {
|
||||
if (root.keys.size() == order - 1) {
|
||||
BPlusTreeNode newRoot = new BPlusTreeNode(false);
|
||||
newRoot.children.add(root);
|
||||
splitChild(newRoot, 0, root);
|
||||
root = newRoot;
|
||||
}
|
||||
insertNonFull(root, key);
|
||||
}
|
||||
|
||||
private void insertNonFull(BPlusTreeNode node, int key) {
|
||||
if (node.isLeaf) {
|
||||
int i = 0;
|
||||
while (i < node.keys.size() && node.keys.get(i) < key) {
|
||||
i++;
|
||||
}
|
||||
node.keys.add(i, key);
|
||||
} else {
|
||||
int i = 0;
|
||||
while (i < node.keys.size() && node.keys.get(i) < key) {
|
||||
i++;
|
||||
}
|
||||
if (node.children.get(i).keys.size() == order - 1) {
|
||||
splitChild(node, i, node.children.get(i));
|
||||
if (node.keys.get(i) < key) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
insertNonFull(node.children.get(i), key);
|
||||
}
|
||||
}
|
||||
|
||||
private void splitChild(BPlusTreeNode parent, int index, BPlusTreeNode fullNode) {
|
||||
BPlusTreeNode newNode = new BPlusTreeNode(fullNode.isLeaf);
|
||||
|
||||
// 移动后半部分键
|
||||
int mid = fullNode.keys.size() / 2;
|
||||
for (int i = mid + (fullNode.isLeaf ? 0 : 1); i < fullNode.keys.size(); i++) {
|
||||
newNode.keys.add(fullNode.keys.get(i));
|
||||
}
|
||||
fullNode.keys.subList(mid + (fullNode.isLeaf ? 0 : 1), fullNode.keys.size()).clear();
|
||||
|
||||
// 移动子节点(如果是内部节点)
|
||||
if (!fullNode.isLeaf) {
|
||||
for (int i = mid + 1; i < fullNode.children.size(); i++) {
|
||||
newNode.children.add(fullNode.children.get(i));
|
||||
}
|
||||
fullNode.children.subList(mid + 1, fullNode.children.size()).clear();
|
||||
}
|
||||
|
||||
// 如果是叶子节点,维护链表
|
||||
if (fullNode.isLeaf) {
|
||||
newNode.next = fullNode.next;
|
||||
fullNode.next = newNode;
|
||||
}
|
||||
|
||||
// 插入到父节点
|
||||
parent.children.add(index + 1, newNode);
|
||||
parent.keys.add(index, fullNode.keys.get(mid));
|
||||
}
|
||||
|
||||
// 查找操作
|
||||
public boolean search(int key) {
|
||||
return search(root, key);
|
||||
}
|
||||
|
||||
private boolean search(BPlusTreeNode node, int key) {
|
||||
int i = 0;
|
||||
while (i < node.keys.size() && node.keys.get(i) < key) {
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i < node.keys.size() && node.keys.get(i) == key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.isLeaf) {
|
||||
return false;
|
||||
} else {
|
||||
return search(node.children.get(i), key);
|
||||
}
|
||||
}
|
||||
|
||||
// 范围查询
|
||||
public List<Integer> rangeSearch(int start, int end) {
|
||||
List<Integer> result = new ArrayList<>();
|
||||
rangeSearch(root, start, end, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void rangeSearch(BPlusTreeNode node, int start, int end, List<Integer> result) {
|
||||
if (node.isLeaf) {
|
||||
for (int key : node.keys) {
|
||||
if (key >= start && key <= end) {
|
||||
result.add(key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int i = 0;
|
||||
while (i < node.keys.size() && node.keys.get(i) < start) {
|
||||
i++;
|
||||
}
|
||||
rangeSearch(node.children.get(i), start, end, result);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
public void delete(int key) {
|
||||
delete(root, key);
|
||||
}
|
||||
|
||||
private void delete(BPlusTreeNode node, int key) {
|
||||
// 实现删除逻辑(简化版)
|
||||
// 实际实现需要处理合并、重新平衡等复杂逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 时间复杂度分析
|
||||
|
||||
### 操作时间复杂度
|
||||
|
||||
| 操作 | 时间复杂度 | 说明 |
|
||||
|------|------------|------|
|
||||
| 查找 | O(log n) | 树高为 O(log n),每层需要比较 O(m) 次 |
|
||||
| 插入 | O(log n) | 查找插入位置 + 可能的分裂操作 |
|
||||
| 删除 | O(log n) | 查找删除位置 + 可能的合并操作 |
|
||||
| 范围查询 | O(log n + k) | k 是结果集大小 |
|
||||
|
||||
### 空间复杂度
|
||||
|
||||
- O(n) - 需要存储 n 个元素
|
||||
- 每个节点存储约 m/2 到 m 个元素
|
||||
|
||||
## 实际应用场景
|
||||
|
||||
### 1. 数据库索引
|
||||
- **MySQL InnoDB**:聚簇索引使用 B+ 树
|
||||
- **PostgreSQL**:标准索引使用 B+ 树
|
||||
- **优点**:范围查询高效,磁盘 I/O 次数少
|
||||
|
||||
### 2. 文件系统
|
||||
- **NTFS**:主文件表使用 B+ 树结构
|
||||
- **ext4**:目录索引使用 B+ 树
|
||||
- **优点**:文件查找效率高,支持大文件系统
|
||||
|
||||
### 3. 内存数据库
|
||||
- **Redis**:有序集合(Sorted Set)使用类似 B+ 树的结构
|
||||
- **LevelDB**:底层存储引擎使用 B+ 树变种
|
||||
- **优点**:内存访问速度更快,但结构保持高效
|
||||
|
||||
### 4. 文件搜索
|
||||
- **全文搜索引擎**:倒排索引使用 B+ 树
|
||||
- **文件系统搜索**:快速定位文件位置
|
||||
- **优点**:前缀查询和范围查询高效
|
||||
|
||||
## 与其他数据结构的对比
|
||||
|
||||
| 数据结构 | 查找时间 | 插入时间 | 删除时间 | 适用场景 |
|
||||
|----------|----------|----------|----------|----------|
|
||||
| B+ 树 | O(log n) | O(log n) | O(log n) | 磁盘存储、数据库索引 |
|
||||
| AVL 树 | O(log n) | O(log n) | O(log n) | 内存存储、需要平衡 |
|
||||
| 红黑树 | O(log n) | O(log n) | O(log n) | 内存存储、平衡性较好 |
|
||||
| 哈希表 | O(1) | O(1) | O(1) | 精确查找、内存存储 |
|
||||
| 二叉搜索树 | O(log n) ~ O(n) | O(log n) ~ O(n) | O(log n) ~ O(n) | 排序数据、简单应用 |
|
||||
|
||||
### 选择 B+ 树的原因
|
||||
|
||||
1. **磁盘友好**:减少磁盘 I/O 次数
|
||||
2. **范围查询高效**:叶子节点链表支持快速范围扫描
|
||||
3. **局部性原理**:每次读取整个页面,充分利用磁盘预读
|
||||
4. **稳定性能**:即使树不平衡,性能下降缓慢
|
||||
5. **批量操作**:适合批量插入和删除
|
||||
|
||||
### 不同场景的最佳选择
|
||||
|
||||
- **内存数据**:AVL 树或红黑树
|
||||
- **磁盘数据**:B+ 树
|
||||
- **精确查找**:哈希表
|
||||
- **范围查询**:B+ 树
|
||||
- **频繁插入删除**:B+ 树或红黑树
|
||||
|
||||
## 常见面试问题
|
||||
|
||||
### Q1: 为什么数据库索引使用 B+ 树而不是 B 树?
|
||||
**答**:
|
||||
1. 范围查询更高效:B+ 树的叶子节点形成链表,范围查询只需遍历叶子节点
|
||||
2. 磁盘利用率高:非叶子节点不存储数据,可以存储更多索引键
|
||||
3. 查找稳定:无论查找什么数据,都到达叶子节点,树高相同
|
||||
4. 预读效率高:每次读取一个完整的页面
|
||||
|
||||
### Q2: B+ 树的阶数如何选择?
|
||||
**答**:
|
||||
阶数取决于:
|
||||
- 磁盘块大小:通常等于操作系统页面大小(4KB)
|
||||
- 键值大小:整数 vs 字符串
|
||||
- 指针大小:64位系统 8 字节指针
|
||||
- 缓存大小:内存缓存页面数量
|
||||
|
||||
### Q3: B+ 树在什么情况下性能下降?
|
||||
**答**:
|
||||
1. 树太高:查找路径变长
|
||||
2. 叶子节点过多:范围查询变慢
|
||||
3. 内存不足:频繁磁盘 I/O
|
||||
4. 数据分布不均:导致树不平衡
|
||||
|
||||
### Q4: 如何优化 B+ 树的性能?
|
||||
**答**:
|
||||
1. 选择合适的阶数:平衡磁盘 I/O 和内存使用
|
||||
2. 使用缓存:缓存热点数据
|
||||
3. 预加载:提前加载可能访问的节点
|
||||
4. 批量操作:合并多次插入/删除操作
|
||||
5. 数据分片:大表分片减少树的大小
|
||||
Reference in New Issue
Block a user