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. 数据分片:大表分片减少树的大小
|
||||
845
questions/08-算法与数据结构/时间轮算法.md
Normal file
845
questions/08-算法与数据结构/时间轮算法.md
Normal file
@@ -0,0 +1,845 @@
|
||||
# 时间轮算法
|
||||
|
||||
## 数据结构原理
|
||||
|
||||
### 什么是时间轮?
|
||||
时间轮(Timing Wheel)是一种用于定时任务调度的数据结构,通过环形队列和层级结构实现高效的时间管理。它特别适合处理大量延迟任务和周期性任务,在分布式系统中广泛应用。
|
||||
|
||||
### 时间轮的核心概念
|
||||
|
||||
1. **轮槽(Slot)**:时间轮的基本单位,每个槽代表一个时间片
|
||||
2. **指针(Pointer)**:当前时间槽的指针,顺时针移动
|
||||
3. **任务(Task)**:需要执行的任务,包含执行时间信息
|
||||
4. **层级结构**:多级时间轮处理不同时间跨度的任务
|
||||
|
||||
### 时间轮的工作原理
|
||||
|
||||
1. **任务添加**:根据任务延迟时间计算放入的槽位
|
||||
2. **任务执行**:指针到达槽位时,执行该槽位所有任务
|
||||
3. **指针移动**:每过一个时间片,指针移动到下一个槽位
|
||||
4. **延迟计算**:任务延迟时间 = 当前时间到执行时间的差值
|
||||
|
||||
## 图解说明
|
||||
|
||||
```
|
||||
单层时间轮示例(槽位数=8,时间片=1秒):
|
||||
0 1 2 3 4 5 6 7
|
||||
+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
| | | | | | | | |
|
||||
|task1|task2| |task3| | |task4| |
|
||||
| | | | | | | | |
|
||||
+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
↑
|
||||
指针
|
||||
当前时间:0秒
|
||||
```
|
||||
|
||||
### 多层时间轮示例
|
||||
|
||||
```
|
||||
多层时间轮(3层):
|
||||
Layer 3 (1小时/槽): [0][1][2][3][4][5] -> 当前:0
|
||||
Layer 2 (1分钟/槽): [0][1][2][3][4][5][6][7] -> 当前:0
|
||||
Layer 1 (1秒/槽): [0][1][2][3][4][5][6][7] -> 当前:0
|
||||
Layer 0 (100ms/槽): [0][1][2][3][4][5][6][7] -> 当前:0
|
||||
```
|
||||
|
||||
### 任务添加流程
|
||||
|
||||
```
|
||||
添加任务(延迟 350ms):
|
||||
1. Layer 0: 350ms / 100ms = 3.5 -> 放入槽 4
|
||||
2. 如果当前指针 > 槽位,放入上一层
|
||||
3. 继续处理,直到找到合适的层级
|
||||
```
|
||||
|
||||
## Java 代码实现
|
||||
|
||||
### 基础时间轮实现
|
||||
|
||||
```java
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class TimingWheel {
|
||||
private final int slotSize; // 槽位数量
|
||||
private final long timePerSlot; // 每个槽位的时间间隔(毫秒)
|
||||
private final List<TimerTask>[] slots; // 时间轮槽
|
||||
private final AtomicInteger currentSlot; // 当前槽位索引
|
||||
private final ExecutorService executor; // 任务执行器
|
||||
private final TimingWheel overflowWheel; // 上层时间轮
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public TimingWheel(int slotSize, long timePerSlot, ExecutorService executor) {
|
||||
this.slotSize = slotSize;
|
||||
this.timePerSlot = timePerSlot;
|
||||
this.slots = (List<TimerTask>[]) new List[slotSize];
|
||||
this.currentSlot = new AtomicInteger(0);
|
||||
this.executor = executor;
|
||||
this.overflowWheel = null;
|
||||
|
||||
for (int i = 0; i < slotSize; i++) {
|
||||
slots[i] = new ArrayList<>();
|
||||
}
|
||||
|
||||
// 启动时间轮线程
|
||||
new Thread(this::rotate).start();
|
||||
}
|
||||
|
||||
// 添加任务
|
||||
public void addTask(TimerTask task) {
|
||||
if (task.getDelay() <= timePerSlot) {
|
||||
int targetSlot = (currentSlot.get() + (int)(task.getDelay() / timePerSlot)) % slotSize;
|
||||
slots[targetSlot].add(task);
|
||||
} else {
|
||||
// 处理跨槽位任务
|
||||
if (overflowWheel == null) {
|
||||
// 创建上层时间轮
|
||||
overflowWheel = new TimingWheel(slotSize, timePerSlot * slotSize, executor);
|
||||
}
|
||||
overflowWheel.addTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
// 时间轮旋转
|
||||
private void rotate() {
|
||||
while (true) {
|
||||
try {
|
||||
Thread.sleep(timePerSlot);
|
||||
|
||||
// 获取当前槽位
|
||||
int slot = currentSlot.getAndIncrement() % slotSize;
|
||||
|
||||
// 执行槽位中的任务
|
||||
List<TimerTask> tasks = slots[slot];
|
||||
if (!tasks.isEmpty()) {
|
||||
List<TimerTask> taskList = new ArrayList<>(tasks);
|
||||
tasks.clear();
|
||||
|
||||
for (TimerTask task : taskList) {
|
||||
executor.submit(() -> task.execute());
|
||||
}
|
||||
}
|
||||
|
||||
// 处理溢出任务
|
||||
if (overflowWheel != null) {
|
||||
overflowWheel.rotateTasks();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 旋转溢出任务
|
||||
private void rotateTasks() {
|
||||
// 从当前槽位取出的任务
|
||||
}
|
||||
}
|
||||
|
||||
// 定时任务接口
|
||||
interface TimerTask {
|
||||
long getDelay(); // 获取延迟时间(毫秒)
|
||||
void execute(); // 执行任务
|
||||
}
|
||||
|
||||
// 具体任务实现
|
||||
class DelayedTask implements TimerTask {
|
||||
private final long delay;
|
||||
private final Runnable task;
|
||||
|
||||
public DelayedTask(long delay, Runnable task) {
|
||||
this.delay = delay;
|
||||
this.task = task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDelay() {
|
||||
return delay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 增强型时间轮实现
|
||||
|
||||
```java
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import java.util.function.*;
|
||||
|
||||
public class EnhancedTimingWheel {
|
||||
private final int tier; // 当前层级
|
||||
private final int slotSize; // 槽位数量
|
||||
private final long timePerSlot; // 每个槽位的时间间隔
|
||||
private final long wheelTimeout; // 轮超时时间
|
||||
private final List<TimerTaskEntry>[] slots; // 时间轮槽
|
||||
private final AtomicInteger currentSlot; // 当前槽位索引
|
||||
private final ExecutorService executor; // 任务执行器
|
||||
private final EnhancedTimingWheel overflowWheel; // 上层时间轮
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public EnhancedTimingWheel(int tier, int slotSize, long timePerSlot,
|
||||
ExecutorService executor) {
|
||||
this.tier = tier;
|
||||
this.slotSize = slotSize;
|
||||
this.timePerSlot = timePerSlot;
|
||||
this.wheelTimeout = slotSize * timePerSlot;
|
||||
this.executor = executor;
|
||||
this.slots = (List<TimerTaskEntry>[]) new List[slotSize];
|
||||
this.currentSlot = new AtomicInteger(0);
|
||||
|
||||
for (int i = 0; i < slotSize; i++) {
|
||||
slots[i] = new CopyOnWriteArrayList<>();
|
||||
}
|
||||
|
||||
if (tier > 0) {
|
||||
this.overflowWheel = new EnhancedTimingWheel(tier - 1, slotSize,
|
||||
timePerSlot * slotSize, executor);
|
||||
} else {
|
||||
this.overflowWheel = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 任务条目
|
||||
public static class TimerTaskEntry {
|
||||
private final TimerTask task;
|
||||
private final long expiration;
|
||||
private TimerTaskEntry next;
|
||||
private volatile boolean cancelled = false;
|
||||
|
||||
public TimerTaskEntry(TimerTask task, long expiration) {
|
||||
this.task = task;
|
||||
this.expiration = expiration;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return expiration <= System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动时间轮
|
||||
public void start() {
|
||||
if (running.compareAndSet(false, true)) {
|
||||
new Thread(this::rotate).start();
|
||||
}
|
||||
}
|
||||
|
||||
// 添加任务
|
||||
public void addTask(TimerTask task, long delay) {
|
||||
long expiration = System.currentTimeMillis() + delay;
|
||||
TimerTaskEntry entry = new TimerTaskEntry(task, expiration);
|
||||
|
||||
if (delay < wheelTimeout) {
|
||||
int targetSlot = (int)((expiration / timePerSlot) % slotSize);
|
||||
slots[targetSlot].add(entry);
|
||||
} else {
|
||||
if (overflowWheel != null) {
|
||||
overflowWheel.addTask(task, delay);
|
||||
} else {
|
||||
// 延迟太长,直接安排执行
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
Thread.sleep(delay);
|
||||
task.execute();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取消任务
|
||||
public boolean cancelTask(TimerTask task) {
|
||||
// 简化的实现,实际需要遍历所有槽位
|
||||
return false;
|
||||
}
|
||||
|
||||
// 时间轮旋转
|
||||
private void rotate() {
|
||||
while (running.get()) {
|
||||
try {
|
||||
Thread.sleep(timePerSlot);
|
||||
|
||||
int slot = currentSlot.getAndIncrement() % slotSize;
|
||||
List<TimerTaskEntry> tasks = slots[slot];
|
||||
|
||||
if (!tasks.isEmpty()) {
|
||||
List<TimerTaskEntry> expiredTasks = new ArrayList<>();
|
||||
List<TimerTaskEntry> activeTasks = new ArrayList<>();
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
for (TimerTaskEntry entry : tasks) {
|
||||
if (entry.isExpired() && !entry.isCancelled()) {
|
||||
expiredTasks.add(entry);
|
||||
} else {
|
||||
activeTasks.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// 清空当前槽位
|
||||
tasks.clear();
|
||||
tasks.addAll(activeTasks);
|
||||
|
||||
// 执行过期任务
|
||||
for (TimerTaskEntry entry : expiredTasks) {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
entry.task.execute();
|
||||
} catch (Exception e) {
|
||||
// 处理异常
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理溢出任务
|
||||
if (overflowWheel != null) {
|
||||
overflowWheel.rotateExpiredTasks();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 旋转过期任务
|
||||
private void rotateExpiredTasks() {
|
||||
// 实现溢出任务的处理
|
||||
}
|
||||
|
||||
// 停止时间轮
|
||||
public void stop() {
|
||||
running.set(false);
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
public TimingWheelStats getStats() {
|
||||
TimingWheelStats stats = new TimingWheelStats();
|
||||
stats.tier = tier;
|
||||
stats.slotSize = slotSize;
|
||||
stats.currentSlot = currentSlot.get();
|
||||
stats.totalTasks = Arrays.stream(slots).mapToInt(List::size).sum();
|
||||
return stats;
|
||||
}
|
||||
|
||||
public static class TimingWheelStats {
|
||||
public int tier;
|
||||
public int slotSize;
|
||||
public int currentSlot;
|
||||
public int totalTasks;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 分布式时间轮实现
|
||||
|
||||
```java
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
|
||||
public class DistributedTimingWheel {
|
||||
private final String nodeId;
|
||||
private final int slotSize;
|
||||
private final long timePerSlot;
|
||||
private final ExecutorService executor;
|
||||
private final Map<String, TimerTaskEntry> taskMap = new ConcurrentHashMap<>();
|
||||
private final TimerWheel[] wheels;
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
||||
public DistributedTimingWheel(String nodeId, int slotSize, long timePerSlot,
|
||||
ExecutorService executor) {
|
||||
this.nodeId = nodeId;
|
||||
this.slotSize = slotSize;
|
||||
this.timePerSlot = timePerSlot;
|
||||
this.executor = executor;
|
||||
this.wheels = new TimerWheel[3]; // 3层时间轮
|
||||
|
||||
// 初始化时间轮
|
||||
for (int i = 0; i < 3; i++) {
|
||||
long slotTime = timePerSlot * (long) Math.pow(slotSize, i);
|
||||
wheels[i] = new TimerWheel(i, slotSize, slotTime, executor);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加分布式任务
|
||||
public void addDistributedTask(String taskId, TimerTask task, long delay) {
|
||||
String taskKey = nodeId + ":" + taskId;
|
||||
TimerTaskEntry entry = new TimerTaskEntry(task, System.currentTimeMillis() + delay);
|
||||
taskMap.put(taskKey, entry);
|
||||
|
||||
// 添加到最合适的时间轮
|
||||
for (int i = wheels.length - 1; i >= 0; i--) {
|
||||
if (delay <= wheels[i].getWheelTimeout()) {
|
||||
wheels[i].addTask(entry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取消任务
|
||||
public boolean cancelTask(String taskId) {
|
||||
String taskKey = nodeId + ":" + taskId;
|
||||
TimerTaskEntry entry = taskMap.remove(taskKey);
|
||||
if (entry != null) {
|
||||
entry.cancel();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 启动时间轮
|
||||
public void start() {
|
||||
if (running.compareAndSet(false, true)) {
|
||||
for (TimerWheel wheel : wheels) {
|
||||
wheel.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 停止时间轮
|
||||
public void stop() {
|
||||
if (running.compareAndSet(true, false)) {
|
||||
for (TimerWheel wheel : wheels) {
|
||||
wheel.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内部时间轮类
|
||||
private class TimerWheel {
|
||||
private final int tier;
|
||||
private final int slotSize;
|
||||
private final long timePerSlot;
|
||||
private final long wheelTimeout;
|
||||
private final List<TimerTaskEntry>[] slots;
|
||||
private final AtomicInteger currentSlot;
|
||||
private final ExecutorService executor;
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public TimerWheel(int tier, int slotSize, long timePerSlot,
|
||||
ExecutorService executor) {
|
||||
this.tier = tier;
|
||||
this.slotSize = slotSize;
|
||||
this.timePerSlot = timePerSlot;
|
||||
this.wheelTimeout = slotSize * timePerSlot;
|
||||
this.executor = executor;
|
||||
this.slots = (List<TimerTaskEntry>[]) new List[slotSize];
|
||||
this.currentSlot = new AtomicInteger(0);
|
||||
|
||||
for (int i = 0; i < slotSize; i++) {
|
||||
slots[i] = new CopyOnWriteArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public void addTask(TimerTaskEntry entry) {
|
||||
long remainingDelay = entry.getExpiration() - System.currentTimeMillis();
|
||||
if (remainingDelay <= timePerSlot) {
|
||||
int targetSlot = (int)((entry.getExpiration() / timePerSlot) % slotSize);
|
||||
slots[targetSlot].add(entry);
|
||||
} else {
|
||||
// 转发到更高层级的时间轮
|
||||
if (tier < wheels.length - 1) {
|
||||
wheels[tier + 1].addTask(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (running.compareAndSet(false, true)) {
|
||||
new Thread(this::rotate).start();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running.set(false);
|
||||
}
|
||||
|
||||
private void rotate() {
|
||||
while (running.get()) {
|
||||
try {
|
||||
Thread.sleep(timePerSlot);
|
||||
int slot = currentSlot.getAndIncrement() % slotSize;
|
||||
|
||||
List<TimerTaskEntry> tasks = slots[slot];
|
||||
if (!tasks.isEmpty()) {
|
||||
List<TimerTaskEntry> expiredTasks = new ArrayList<>();
|
||||
List<TimerTaskEntry> activeTasks = new ArrayList<>();
|
||||
|
||||
for (TimerTaskEntry entry : tasks) {
|
||||
if (entry.isExpired()) {
|
||||
expiredTasks.add(entry);
|
||||
} else {
|
||||
activeTasks.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
tasks.clear();
|
||||
tasks.addAll(activeTasks);
|
||||
|
||||
for (TimerTaskEntry entry : expiredTasks) {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
entry.getTask().execute();
|
||||
// 从任务映射中移除
|
||||
taskMap.remove(entry.getTaskId());
|
||||
} catch (Exception e) {
|
||||
// 处理异常
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long getWheelTimeout() {
|
||||
return wheelTimeout;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 时间复杂度分析
|
||||
|
||||
### 操作时间复杂度
|
||||
|
||||
| 操作 | 时间复杂度 | 说明 |
|
||||
|------|------------|------|
|
||||
| 添加任务 | O(1) | 直接计算槽位并添加 |
|
||||
| 取消任务 | O(1) | 标记任务为取消状态 |
|
||||
| 执行任务 | O(1) | 指针到达槽位时执行 |
|
||||
| 旋转时间轮 | O(1) | 移动指针,检查槽位 |
|
||||
|
||||
### 空间复杂度
|
||||
|
||||
- O(n) - 存储 n 个任务
|
||||
- 时间轮槽位使用固定空间 O(slotSize)
|
||||
|
||||
### 性能特点
|
||||
|
||||
1. **时间轮旋转**:每次旋转 O(1) 时间复杂度
|
||||
2. **任务执行**:平均每个任务执行 O(1) 时间
|
||||
3. **内存使用**:固定大小的时间轮,内存可控
|
||||
4. **并发性能**:使用 CopyOnWriteArrayList 保证线程安全
|
||||
|
||||
## 实际应用场景
|
||||
|
||||
### 1. 分布式任务调度
|
||||
- **定时任务**:周期性任务执行
|
||||
- **延迟任务**:延迟执行的任务
|
||||
- **任务重试**:失败任务的重试机制
|
||||
|
||||
```java
|
||||
// 分布式任务调度示例
|
||||
public class TaskScheduler {
|
||||
private final DistributedTimingWheel timingWheel;
|
||||
private final ExecutorService executor;
|
||||
|
||||
public void scheduleTask(String taskId, Runnable task, long delay) {
|
||||
timingWheel.addDistributedTask(taskId, () -> {
|
||||
try {
|
||||
task.run();
|
||||
} catch (Exception e) {
|
||||
// 任务执行失败,可以重试
|
||||
scheduleRetry(taskId, task, delay);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
private void scheduleRetry(String taskId, Runnable task, long delay) {
|
||||
// 实现重试逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 消息队列超时处理
|
||||
- **消息超时**:处理超时的消息
|
||||
- **死信队列**:移除超时未消费的消息
|
||||
- **重试机制**:消息重试调度
|
||||
|
||||
```java
|
||||
// 消息队列超时处理
|
||||
public class MessageQueueTimeoutHandler {
|
||||
private final EnhancedTimingWheel timingWheel;
|
||||
|
||||
public void handleMessageTimeout(String messageId, long timeout) {
|
||||
timingWheel.addTask(() -> {
|
||||
// 处理超时消息
|
||||
handleTimeoutMessage(messageId);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
private void handleTimeoutMessage(String messageId) {
|
||||
// 将消息移到死信队列
|
||||
System.out.println("Message " + messageId + " timed out");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 缓存过期清理
|
||||
- **缓存过期**:清理过期的缓存数据
|
||||
- **惰性删除**:被动删除过期数据
|
||||
- **定时删除**:主动删除过期数据
|
||||
|
||||
```java
|
||||
// 缓存过期清理
|
||||
public class CacheManager {
|
||||
private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
|
||||
private final EnhancedTimingWheel timingWheel;
|
||||
|
||||
public void put(String key, Object value, long ttl) {
|
||||
CacheEntry entry = new CacheEntry(value, System.currentTimeMillis() + ttl);
|
||||
cache.put(key, entry);
|
||||
|
||||
// 添加到时间轮
|
||||
timingWheel.addTask(() -> {
|
||||
cache.remove(key);
|
||||
}, ttl);
|
||||
}
|
||||
|
||||
private static class CacheEntry {
|
||||
private final Object value;
|
||||
private final long expiration;
|
||||
|
||||
public CacheEntry(Object value, long expiration) {
|
||||
this.value = value;
|
||||
this.expiration = expiration;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 连接池管理
|
||||
- **连接超时**:检测和关闭超时连接
|
||||
- **空闲连接**:清理长时间空闲的连接
|
||||
- **连接保活**:定期检查连接状态
|
||||
|
||||
```java
|
||||
// 连接池管理
|
||||
public class ConnectionPool {
|
||||
private final Map<String, Connection> connections = new ConcurrentHashMap<>();
|
||||
private final EnhancedTimingWheel timingWheel;
|
||||
|
||||
public void addConnection(String id, Connection connection, long timeout) {
|
||||
connections.put(id, connection);
|
||||
|
||||
// 添加超时检测
|
||||
timingWheel.addTask(() -> {
|
||||
Connection conn = connections.get(id);
|
||||
if (conn != null && conn.isIdle()) {
|
||||
conn.close();
|
||||
connections.remove(id);
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. API 限流
|
||||
- **限流窗口**:控制 API 调用频率
|
||||
- **时间窗口**:基于时间窗口的限流
|
||||
- **滑动窗口**:实现滑动限流算法
|
||||
|
||||
```java
|
||||
// API 限流实现
|
||||
public class RateLimiter {
|
||||
private final EnhancedTimingWheel timingWheel;
|
||||
private final Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
|
||||
|
||||
public boolean allowRequest(String userId, long windowSize, int limit) {
|
||||
String counterKey = userId + ":" + System.currentTimeMillis() / windowSize;
|
||||
AtomicInteger counter = counters.computeIfAbsent(counterKey, k -> new AtomicInteger(0));
|
||||
|
||||
timingWheel.addTask(() -> counters.remove(counterKey), windowSize);
|
||||
|
||||
return counter.incrementAndGet() <= limit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 与其他调度方式的对比
|
||||
|
||||
| 调度方式 | 时间复杂度 | 内存使用 | 适用场景 | 优点 | 缺点 |
|
||||
|----------|------------|----------|----------|------|------|
|
||||
| 时间轮 | O(1) | O(n) | 大量延迟任务 | 高效、内存可控 | 实现复杂 |
|
||||
| 优先队列 | O(log n) | O(n) | 任务数量较少 | 实现简单 | 性能较差 |
|
||||
| 线程池 | O(1) | O(n) | 短期任务 | 使用简单 | 资源消耗大 |
|
||||
| 定时器 | O(1) | O(1) | 少量任务 | API 简单 | 不适合大量任务 |
|
||||
| NIO | O(1) | O(n) | 高并发 | 性能极高 | 实现复杂 |
|
||||
|
||||
### 时间轮的优势
|
||||
|
||||
1. **高效性**:O(1) 时间复杂度的任务调度
|
||||
2. **内存可控**:固定大小的时间轮结构
|
||||
3. **实时性**:精确的任务执行时间控制
|
||||
4. **扩展性**:支持多层级处理复杂任务
|
||||
5. **并发友好**:线程安全的实现
|
||||
|
||||
### 时间轮的劣势
|
||||
|
||||
1. **实现复杂**:相比其他方式实现较复杂
|
||||
2. **精度限制**:受时间片大小限制
|
||||
3. **内存开销**:需要维护多个时间轮
|
||||
4. **任务取消**:取消任务需要额外处理
|
||||
|
||||
## 常见面试问题
|
||||
|
||||
### Q1: 时间轮和优先队列有什么区别?为什么选择时间轮?
|
||||
**答**:
|
||||
**主要区别**:
|
||||
1. **时间复杂度**:时间轮 O(1),优先队列 O(log n)
|
||||
2. **内存使用**:时间轮固定大小,优先队列动态增长
|
||||
3. **任务处理**:时间轮轮询,优先队列堆排序
|
||||
4. **实现复杂度**:时间轮较复杂,优先队列简单
|
||||
|
||||
**选择时间轮的原因**:
|
||||
- 任务数量大时性能更好
|
||||
- 内存使用更可控
|
||||
- 适合处理大量延迟任务
|
||||
- 支持高并发场景
|
||||
|
||||
### Q2: 如何处理时间轮的精度问题?
|
||||
**答**:
|
||||
**精度优化策略**:
|
||||
1. **调整时间片**:根据需求选择合适的时间片大小
|
||||
2. **多层级时间轮**:小时间片处理短期任务,大时间片处理长期任务
|
||||
3. **实时校准**:定期校准系统时间
|
||||
4. **任务优先级**:高优先级任务单独处理
|
||||
5. **补偿机制**:记录实际执行时间进行补偿
|
||||
|
||||
### Q3: 如何处理时间轮中的任务取消?
|
||||
**答**:
|
||||
**取消机制实现**:
|
||||
1. **标记机制**:在任务条目中设置取消标志
|
||||
2. **垃圾回收**:定期清理已取消的任务
|
||||
3. **主动查询**:提供取消任务的接口
|
||||
4. **批量清理**:在时间轮旋转时批量清理
|
||||
5. **超时自动清理**:长时间未执行的任务自动清理
|
||||
|
||||
```java
|
||||
// 任务取消实现
|
||||
public boolean cancelTask(String taskId) {
|
||||
String taskKey = nodeId + ":" + taskId;
|
||||
TimerTaskEntry entry = taskMap.get(taskKey);
|
||||
if (entry != null) {
|
||||
entry.cancel();
|
||||
// 可以选择立即从任务映射中移除
|
||||
taskMap.remove(taskKey);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
### Q4: 时间轮在分布式环境中如何保证一致性?
|
||||
**答**:
|
||||
**分布式一致性方案**:
|
||||
1. **任务迁移**:任务在节点间迁移时保持一致性
|
||||
2. **时间同步**:所有节点使用同步时钟
|
||||
3. **任务分发**:根据任务类型和节点负载分发任务
|
||||
4. **故障恢复**:节点故障时任务重新分发
|
||||
5. **共识机制**:使用一致性协议保证任务不丢失
|
||||
|
||||
### Q5: 如何优化时间轮的性能?
|
||||
**答**:
|
||||
**性能优化策略**:
|
||||
1. **时间轮层级**:合理设置时间轮层级数量
|
||||
2. **槽位数量**:根据任务分布调整槽位数量
|
||||
3. **任务批处理**:批量处理相似任务
|
||||
4. **线程优化**:使用多线程处理任务执行
|
||||
5. **内存优化**:使用更高效的数据结构
|
||||
6. **缓存优化**:优化热点数据的访问
|
||||
|
||||
### Q6: 时间轮如何处理大量任务?
|
||||
**答**:
|
||||
**大量任务处理方案**:
|
||||
1. **任务分片**:将任务分散到不同时间轮
|
||||
2. **分层处理**:短期任务和长期任务分别处理
|
||||
3. **任务合并**:相似任务合并执行
|
||||
4. **负载均衡**:多个时间轮实例并行工作
|
||||
5. **资源管理**:动态调整时间轮资源分配
|
||||
|
||||
### Q7: 时间轮和事件驱动模式有什么关系?
|
||||
**答**:
|
||||
**关系说明**:
|
||||
1. **结合使用**:时间轮可以作为事件驱动系统的一部分
|
||||
2. **触发机制**:时间轮可以触发事件的执行
|
||||
3. **定时事件**:时间轮专门处理定时事件
|
||||
4. **互补关系**:事件驱动处理即时事件,时间轮处理延迟事件
|
||||
5. **性能协同**:两者结合可以提高系统整体性能
|
||||
|
||||
### Q8: 如何实现任务的优先级处理?
|
||||
**答**:
|
||||
**优先级实现方案**:
|
||||
1. **多个时间轮**:不同优先级使用不同时间轮
|
||||
2. **优先队列**:在时间轮中使用优先队列
|
||||
3. **任务标记**:高优先级任务特殊标记
|
||||
4. **抢占执行**:高优先级任务可以抢占低优先级任务
|
||||
5. **动态调整**:根据任务优先级动态调整执行顺序
|
||||
|
||||
```java
|
||||
// 带优先级的时间轮
|
||||
public class PriorityTimingWheel {
|
||||
private final Map<Integer, EnhancedTimingWheel> priorityWheels = new ConcurrentHashMap<>();
|
||||
|
||||
public void addTask(TimerTask task, long delay, int priority) {
|
||||
EnhancedTimingWheel wheel = priorityWheels.computeIfAbsent(
|
||||
priority,
|
||||
p -> new EnhancedTimingWheel(3, 8, 100, executor)
|
||||
);
|
||||
wheel.addTask(task, delay);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Q9: 时间轮的内存如何管理?
|
||||
**答**:
|
||||
**内存管理策略**:
|
||||
1. **固定大小**:时间轮槽位数量固定
|
||||
2. **对象复用**:复用任务对象减少 GC
|
||||
3. **分代管理**:不同生命周期对象分别管理
|
||||
4. **内存监控**:监控内存使用情况
|
||||
5. **自动扩容**:在必要时自动扩展时间轮
|
||||
|
||||
### Q10: 时间轮的适用场景和限制是什么?
|
||||
**答**:
|
||||
**适用场景**:
|
||||
1. **大量延迟任务**:需要处理大量延迟任务
|
||||
2. **高并发环境**:需要高并发处理能力
|
||||
3. **内存受限环境**:内存使用需要可控
|
||||
4. **实时性要求**:需要精确的时间控制
|
||||
5. **分布式系统**:需要分布式任务调度
|
||||
|
||||
**限制**:
|
||||
1. **精度限制**:受时间片大小限制
|
||||
2. **实现复杂**:相比其他实现较复杂
|
||||
3. **单机限制**:单机处理能力有限
|
||||
4. **任务取消**:取消任务处理复杂
|
||||
5. **时间同步**:分布式环境需要时间同步
|
||||
|
||||
## 总结
|
||||
|
||||
时间轮算法是一种高效的时间调度算法,特别适合处理大量延迟任务和周期性任务。通过合理的设计和优化,可以在各种场景下实现高性能的任务调度。理解时间轮的原理和实现方式,对于设计和实现高性能的分布式系统具有重要意义。
|
||||
623
questions/08-算法与数据结构/红黑树原理.md
Normal file
623
questions/08-算法与数据结构/红黑树原理.md
Normal file
@@ -0,0 +1,623 @@
|
||||
# 红黑树 (Red-Black Tree)
|
||||
|
||||
## 数据结构原理
|
||||
|
||||
### 什么是红黑树?
|
||||
红黑树是一种自平衡的二叉搜索树,通过在每个节点上增加一个颜色属性(红色或黑色)来保证树的平衡性。它能够在 O(log n) 时间内完成查找、插入和删除操作,是平衡二叉搜索树的一种。
|
||||
|
||||
### 红黑树的特性
|
||||
|
||||
1. **节点颜色**:每个节点要么是红色,要么是黑色
|
||||
2. **根节点**:根节点总是黑色
|
||||
3. **叶子节点**:所有叶子节点(NIL 节点)都是黑色
|
||||
4. **红色节点**:红色节点的子节点必须是黑色
|
||||
5. **路径长度**:从任一节点到其每个叶子节点的所有路径都包含相同数量的黑色节点
|
||||
|
||||
### 红黑树的平衡保证
|
||||
|
||||
红黑树通过维护这些性质,确保:
|
||||
- 树的高度最多为 2log(n+1)
|
||||
- 最坏情况下时间复杂度为 O(log n)
|
||||
- 插入和删除操作的最坏时间复杂度为 O(log n)
|
||||
|
||||
## 图解说明
|
||||
|
||||
```
|
||||
红黑树结构示例:
|
||||
|
||||
●(13) // 黑色节点
|
||||
/ \
|
||||
○(8) ●(17)
|
||||
/ \ / \
|
||||
●(1) ●(11) ○(15) ●(25)
|
||||
/ \ / \ / \
|
||||
NIL NIL NIL NIL NIL NIL
|
||||
```
|
||||
|
||||
### 节点表示
|
||||
|
||||
```
|
||||
颜色表示:
|
||||
● - 黑色节点
|
||||
○ - 红色节点
|
||||
[NIL] - 黑色叶子节点
|
||||
```
|
||||
|
||||
### 平衡性质说明
|
||||
|
||||
1. **性质1**:每个节点是红色或黑色
|
||||
2. **性质2**:根节点是黑色
|
||||
3. **性质3**:所有叶子节点都是黑色(NIL 节点)
|
||||
4. **性质4**:红色节点的子节点都是黑色
|
||||
5. **性质5**:从任一节点到其每个叶子节点的所有路径包含相同数量的黑色节点
|
||||
|
||||
## Java 代码实现
|
||||
|
||||
### 节点类定义
|
||||
|
||||
```java
|
||||
enum Color {
|
||||
RED, BLACK
|
||||
}
|
||||
|
||||
class RedBlackNode<T extends Comparable<T>> {
|
||||
T key;
|
||||
Color color;
|
||||
RedBlackNode<T> left, right, parent;
|
||||
|
||||
public RedBlackNode(T key) {
|
||||
this.key = key;
|
||||
this.color = Color.RED; // 新节点默认为红色
|
||||
this.left = this.right = this.parent = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 红黑树实现
|
||||
|
||||
```java
|
||||
public class RedBlackTree<T extends Comparable<T>> {
|
||||
private RedBlackNode<T> root;
|
||||
private RedBlackNode<T> NIL; // 哨兵节点
|
||||
|
||||
public RedBlackTree() {
|
||||
NIL = new RedBlackNode<>(null);
|
||||
NIL.color = Color.BLACK;
|
||||
root = NIL;
|
||||
}
|
||||
|
||||
// 左旋操作
|
||||
private void leftRotate(RedBlackNode<T> x) {
|
||||
RedBlackNode<T> y = x.right;
|
||||
x.right = y.left;
|
||||
|
||||
if (y.left != NIL) {
|
||||
y.left.parent = x;
|
||||
}
|
||||
|
||||
y.parent = x.parent;
|
||||
|
||||
if (x.parent == NIL) {
|
||||
root = y;
|
||||
} else if (x == x.parent.left) {
|
||||
x.parent.left = y;
|
||||
} else {
|
||||
x.parent.right = y;
|
||||
}
|
||||
|
||||
y.left = x;
|
||||
x.parent = y;
|
||||
}
|
||||
|
||||
// 右旋操作
|
||||
private void rightRotate(RedBlackNode<T> y) {
|
||||
RedBlackNode<T> x = y.left;
|
||||
y.left = x.right;
|
||||
|
||||
if (x.right != NIL) {
|
||||
x.right.parent = y;
|
||||
}
|
||||
|
||||
x.parent = y.parent;
|
||||
|
||||
if (y.parent == NIL) {
|
||||
root = x;
|
||||
} else if (y == y.parent.left) {
|
||||
y.parent.left = x;
|
||||
} else {
|
||||
y.parent.right = x;
|
||||
}
|
||||
|
||||
x.right = y;
|
||||
y.parent = x;
|
||||
}
|
||||
|
||||
// 插入操作
|
||||
public void insert(T key) {
|
||||
RedBlackNode<T> newNode = new RedBlackNode<>(key);
|
||||
newNode.left = NIL;
|
||||
newNode.right = NIL;
|
||||
newNode.color = Color.RED;
|
||||
|
||||
// 标准二叉搜索树插入
|
||||
RedBlackNode<T> y = NIL;
|
||||
RedBlackNode<T> x = root;
|
||||
|
||||
while (x != NIL) {
|
||||
y = x;
|
||||
if (newNode.key.compareTo(x.key) < 0) {
|
||||
x = x.left;
|
||||
} else {
|
||||
x = x.right;
|
||||
}
|
||||
}
|
||||
|
||||
newNode.parent = y;
|
||||
|
||||
if (y == NIL) {
|
||||
root = newNode;
|
||||
} else if (newNode.key.compareTo(y.key) < 0) {
|
||||
y.left = newNode;
|
||||
} else {
|
||||
y.right = newNode;
|
||||
}
|
||||
|
||||
// 修复红黑树性质
|
||||
insertFixUp(newNode);
|
||||
}
|
||||
|
||||
// 插入后的修复
|
||||
private void insertFixUp(RedBlackNode<T> z) {
|
||||
while (z.parent.color == Color.RED) {
|
||||
if (z.parent == z.parent.parent.left) {
|
||||
RedBlackNode<T> y = z.parent.parent.right;
|
||||
|
||||
if (y.color == Color.RED) {
|
||||
// 情况1:叔节点是红色
|
||||
z.parent.color = Color.BLACK;
|
||||
y.color = Color.BLACK;
|
||||
z.parent.parent.color = Color.RED;
|
||||
z = z.parent.parent;
|
||||
} else {
|
||||
if (z == z.parent.right) {
|
||||
// 情况2:叔节点是黑色,z 是右孩子
|
||||
z = z.parent;
|
||||
leftRotate(z);
|
||||
}
|
||||
// 情况3:叔节点是黑色,z 是左孩子
|
||||
z.parent.color = Color.BLACK;
|
||||
z.parent.parent.color = Color.RED;
|
||||
rightRotate(z.parent.parent);
|
||||
}
|
||||
} else {
|
||||
// 对称情况
|
||||
RedBlackNode<T> y = z.parent.parent.left;
|
||||
|
||||
if (y.color == Color.RED) {
|
||||
z.parent.color = Color.BLACK;
|
||||
y.color = Color.BLACK;
|
||||
z.parent.parent.color = Color.RED;
|
||||
z = z.parent.parent;
|
||||
} else {
|
||||
if (z == z.parent.left) {
|
||||
z = z.parent;
|
||||
rightRotate(z);
|
||||
}
|
||||
z.parent.color = Color.BLACK;
|
||||
z.parent.parent.color = Color.RED;
|
||||
leftRotate(z.parent.parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
root.color = Color.BLACK;
|
||||
}
|
||||
|
||||
// 查找操作
|
||||
public RedBlackNode<T> search(T key) {
|
||||
RedBlackNode<T> current = root;
|
||||
while (current != NIL && key.compareTo(current.key) != 0) {
|
||||
if (key.compareTo(current.key) < 0) {
|
||||
current = current.left;
|
||||
} else {
|
||||
current = current.right;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
// 中序遍历
|
||||
public void inOrderTraversal() {
|
||||
inOrderTraversal(root);
|
||||
}
|
||||
|
||||
private void inOrderTraversal(RedBlackNode<T> node) {
|
||||
if (node != NIL) {
|
||||
inOrderTraversal(node.left);
|
||||
System.out.print(node.key + (node.color == Color.RED ? "R" : "B") + " ");
|
||||
inOrderTraversal(node.right);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除操作(简化版)
|
||||
public void delete(T key) {
|
||||
RedBlackNode<T> z = search(key);
|
||||
if (z == NIL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 实际的删除实现需要复杂的修复逻辑
|
||||
deleteFixUp(z);
|
||||
}
|
||||
|
||||
// 删除后的修复(简化版)
|
||||
private void deleteFixUp(RedBlackNode<T> x) {
|
||||
// 实际实现需要处理多种情况
|
||||
// 这里简化处理,实际面试中需要详细实现
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完整的删除操作实现
|
||||
|
||||
```java
|
||||
// 删除节点
|
||||
public void delete(T key) {
|
||||
RedBlackNode<T> z = search(key);
|
||||
if (z == NIL) {
|
||||
return;
|
||||
}
|
||||
|
||||
RedBlackNode<T> y = z;
|
||||
RedBlackNode<T> x;
|
||||
Color yOriginalColor = y.color;
|
||||
|
||||
if (z.left == NIL) {
|
||||
x = z.right;
|
||||
transplant(z, z.right);
|
||||
} else if (z.right == NIL) {
|
||||
x = z.left;
|
||||
transplant(z, z.left);
|
||||
} else {
|
||||
y = minimum(z.right);
|
||||
yOriginalColor = y.color;
|
||||
x = y.right;
|
||||
|
||||
if (y.parent == z) {
|
||||
x.parent = y;
|
||||
} else {
|
||||
transplant(y, y.right);
|
||||
y.right = z.right;
|
||||
y.right.parent = y;
|
||||
}
|
||||
|
||||
transplant(z, y);
|
||||
y.left = z.left;
|
||||
y.left.parent = y;
|
||||
y.color = z.color;
|
||||
}
|
||||
|
||||
if (yOriginalColor == Color.BLACK) {
|
||||
deleteFixUp(x);
|
||||
}
|
||||
}
|
||||
|
||||
// 替换节点
|
||||
private void transplant(RedBlackNode<T> u, RedBlackNode<T> v) {
|
||||
if (u.parent == NIL) {
|
||||
root = v;
|
||||
} else if (u == u.parent.left) {
|
||||
u.parent.left = v;
|
||||
} else {
|
||||
u.parent.right = v;
|
||||
}
|
||||
v.parent = u.parent;
|
||||
}
|
||||
|
||||
// 查找最小节点
|
||||
private RedBlackNode<T> minimum(RedBlackNode<T> node) {
|
||||
while (node.left != NIL) {
|
||||
node = node.left;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// 删除修复
|
||||
private void deleteFixUp(RedBlackNode<T> x) {
|
||||
while (x != root && x.color == Color.BLACK) {
|
||||
if (x == x.parent.left) {
|
||||
RedBlackNode<T> w = x.parent.right;
|
||||
|
||||
if (w.color == Color.RED) {
|
||||
// 情况1:兄弟节点是红色
|
||||
w.color = Color.BLACK;
|
||||
x.parent.color = Color.RED;
|
||||
leftRotate(x.parent);
|
||||
w = x.parent.right;
|
||||
}
|
||||
|
||||
if (w.left.color == Color.BLACK && w.right.color == Color.BLACK) {
|
||||
// 情况2:兄弟节点是黑色,且两个子节点都是黑色
|
||||
w.color = Color.RED;
|
||||
x = x.parent;
|
||||
} else {
|
||||
if (w.right.color == Color.BLACK) {
|
||||
// 情况3:兄弟节点是黑色,左子是红色,右子是黑色
|
||||
w.left.color = Color.BLACK;
|
||||
w.color = Color.RED;
|
||||
rightRotate(w);
|
||||
w = x.parent.right;
|
||||
}
|
||||
// 情况4:兄弟节点是黑色,右子是红色
|
||||
w.color = x.parent.color;
|
||||
x.parent.color = Color.BLACK;
|
||||
w.right.color = Color.BLACK;
|
||||
leftRotate(x.parent);
|
||||
x = root;
|
||||
}
|
||||
} else {
|
||||
// 对称情况
|
||||
RedBlackNode<T> w = x.parent.left;
|
||||
|
||||
if (w.color == Color.RED) {
|
||||
w.color = Color.BLACK;
|
||||
x.parent.color = Color.RED;
|
||||
rightRotate(x.parent);
|
||||
w = x.parent.left;
|
||||
}
|
||||
|
||||
if (w.right.color == Color.BLACK && w.left.color == Color.BLACK) {
|
||||
w.color = Color.RED;
|
||||
x = x.parent;
|
||||
} else {
|
||||
if (w.left.color == Color.BLACK) {
|
||||
w.right.color = Color.BLACK;
|
||||
w.color = Color.RED;
|
||||
leftRotate(w);
|
||||
w = x.parent.left;
|
||||
}
|
||||
w.color = x.parent.color;
|
||||
x.parent.color = Color.BLACK;
|
||||
w.left.color = Color.BLACK;
|
||||
rightRotate(x.parent);
|
||||
x = root;
|
||||
}
|
||||
}
|
||||
}
|
||||
x.color = Color.BLACK;
|
||||
}
|
||||
```
|
||||
|
||||
## 时间复杂度分析
|
||||
|
||||
### 操作时间复杂度
|
||||
|
||||
| 操作 | 时间复杂度 | 说明 |
|
||||
|------|------------|------|
|
||||
| 查找 | O(log n) | 平衡二叉搜索树查找 |
|
||||
| 插入 | O(log n) | 查找插入位置 + 修复平衡 |
|
||||
| 删除 | O(log n) | 查找删除位置 + 修复平衡 |
|
||||
| 中序遍历 | O(n) | 遍历所有节点 |
|
||||
| 最值查找 | O(log n) | 到达叶子节点 |
|
||||
|
||||
### 空间复杂度
|
||||
|
||||
- O(n) - 存储 n 个节点
|
||||
- 递归栈空间:O(log n)
|
||||
|
||||
### 性能对比
|
||||
|
||||
| 特性 | 红黑树 | AVL 树 | 二叉搜索树 |
|
||||
|------|--------|--------|------------|
|
||||
| 平衡性 | 相对平衡 | 严格平衡 | 可能不平衡 |
|
||||
| 查找效率 | O(log n) | O(log n) | O(log n) ~ O(n) |
|
||||
| 插入效率 | O(log n) | O(log n) | O(log n) ~ O(n) |
|
||||
| 删除效率 | O(log n) | O(log n) | O(log n) ~ O(n) |
|
||||
| 旋转次数 | 较少 | 较多 | 不需要 |
|
||||
| 适用场景 | 插入频繁 | 查询密集 | 数据有序 |
|
||||
|
||||
## 实际应用场景
|
||||
|
||||
### 1. Java 集合框架
|
||||
- **`java.util.TreeMap`**:使用红黑树实现
|
||||
- **`java.util.TreeSet`**:基于 TreeMap 实现
|
||||
|
||||
```java
|
||||
// TreeMap 使用示例
|
||||
Map<String, Integer> treeMap = new TreeMap<>();
|
||||
treeMap.put("apple", 1);
|
||||
treeMap.put("banana", 2);
|
||||
treeMap.put("orange", 3);
|
||||
|
||||
// 自动排序
|
||||
for (Map.Entry<String, Integer> entry : treeMap.entrySet()) {
|
||||
System.out.println(entry.getKey() + ": " + entry.getValue());
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Linux 内核
|
||||
- ** Completely Fair Scheduler (CFS)**:使用红黑树管理进程调度
|
||||
- **内存管理**:管理虚拟内存区域
|
||||
- **文件系统**:ext3、ext4 使用红黑树
|
||||
|
||||
```c
|
||||
// Linux 内核中的红黑树使用示例
|
||||
struct rb_root root = RB_ROOT;
|
||||
struct my_data *data = rb_entry(node, struct my_data, rb_node);
|
||||
```
|
||||
|
||||
### 3. 数据库索引
|
||||
- **B 树索引**:底层使用类似红黑树的结构
|
||||
- **范围查询**:中序遍历支持范围查询
|
||||
- **排序操作**:自动维护有序性
|
||||
|
||||
```java
|
||||
// 数据库索引示例
|
||||
public class DatabaseIndex {
|
||||
private RedBlackTree<String, List<Record>> index;
|
||||
|
||||
public void insert(String key, Record record) {
|
||||
index.put(key, Collections.singletonList(record));
|
||||
}
|
||||
|
||||
public List<Record> rangeQuery(String start, String end) {
|
||||
// 利用红黑树的中序遍历特性
|
||||
return index.rangeSearch(start, end);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 网络路由
|
||||
- **路由表管理**:IP 地址范围查找
|
||||
- **防火墙规则**:规则匹配和优先级排序
|
||||
|
||||
```java
|
||||
// 路由表示例
|
||||
public class RoutingTable {
|
||||
private RedBlackTree<String, Route> routes;
|
||||
|
||||
public Route findRoute(String ip) {
|
||||
// 查找匹配的路由规则
|
||||
return routes.get(ip);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 与其他平衡树的对比
|
||||
|
||||
| 特性 | 红黑树 | AVL 树 | B 树 | B+ 树 |
|
||||
|------|--------|--------|------|-------|
|
||||
| 平衡因子 | 相对宽松 | 严格平衡 | 多路 | 多路 |
|
||||
| 查找效率 | O(log n) | O(log n) | O(log n) | O(log n) |
|
||||
| 插入效率 | O(log n) | O(log n) | O(log n) | O(log n) |
|
||||
| 旋转次数 | 较少 | 频繁 | 分裂/合并 | 分裂/合并 |
|
||||
| 内存使用 | 较少 | 较少 | 较大 | 较大 |
|
||||
| 适用场景 | 插入频繁 | 查询密集 | 磁盘存储 | 磁盘存储 |
|
||||
| 实现复杂度 | 中等 | 中等 | 复杂 | 复杂 |
|
||||
|
||||
### 选择红黑树的理由
|
||||
|
||||
1. **性能平衡**:插入、删除、查找都是 O(log n)
|
||||
2. **实现相对简单**:比 AVL 树简单,比 B 树简单
|
||||
3. **插入性能好**:插入时旋转次数少于 AVL 树
|
||||
4. **内存效率高**:不需要像 B 树那样维护多路指针
|
||||
5. **广泛应用**:Java 标准库采用,证明其可靠性
|
||||
|
||||
### 不同场景的最佳选择
|
||||
|
||||
- **内存数据结构**:红黑树、AVL 树
|
||||
- **磁盘数据存储**:B 树、B+ 树
|
||||
- **频繁插入**:红黑树
|
||||
- **频繁查询**:AVL 树
|
||||
- **范围查询**:B+ 树(磁盘)、红黑树(内存)
|
||||
|
||||
## 常见面试问题
|
||||
|
||||
### Q1: 红黑树和 AVL 树有什么区别?什么时候选择红黑树?
|
||||
**答**:
|
||||
**主要区别**:
|
||||
1. **平衡程度**:AVL 树严格平衡,红黑树相对宽松
|
||||
2. **旋转频率**:AVL 树旋转更频繁,红黑树较少
|
||||
3. **查找性能**:AVL 树查找更快(树更矮)
|
||||
4. **插入性能**:红黑树插入更快(旋转少)
|
||||
|
||||
**选择红黑树的情况**:
|
||||
- 插入操作比查询操作频繁
|
||||
- 需要保证最坏情况下的性能
|
||||
- 不需要特别严格的平衡
|
||||
- 需要实现简单、高效的平衡树
|
||||
|
||||
### Q2: 红黑树的插入操作如何保证平衡?
|
||||
**答**:
|
||||
插入操作后通过以下方式修复平衡:
|
||||
1. **确定问题节点**:从插入节点开始向上遍历
|
||||
2. **处理三种情况**:
|
||||
- 情况1:叔节点是红色,通过重新着色解决
|
||||
- 情况2:叔节点是黑色,且节点是右孩子,通过左旋调整
|
||||
- 情况3:叔节点是黑色,且节点是左孩子,通过右旋和着色解决
|
||||
|
||||
### Q3: 为什么 Java 的 TreeMap 使用红黑树而不是 AVL 树?
|
||||
**答**:
|
||||
1. **性能考虑**:红黑树插入和删除性能更好
|
||||
2. **平衡性要求**:TreeMap 需要保证整体性能,而不是最优查找
|
||||
3. **实现复杂度**:红黑树实现相对简单
|
||||
4. **历史原因**:早期 Java 版本选择,后续保持兼容性
|
||||
|
||||
### Q4: 红黑树的时间复杂度是如何保证的?
|
||||
**答**:
|
||||
通过以下性质保证:
|
||||
1. **路径长度限制**:任何路径的黑色节点数量相同
|
||||
2. **最长路径限制**:最长路径不超过最短路径的 2 倍
|
||||
3. **树高控制**:树高 h ≤ 2log₂(n+1)
|
||||
4. **操作效率**:每次操作最多 O(log n) 次旋转
|
||||
|
||||
### Q5: 如何验证红黑树的正确性?
|
||||
**答**:
|
||||
验证五条性质:
|
||||
1. 每个节点是红色或黑色
|
||||
2. 根节点是黑色
|
||||
3. 所有叶子节点是黑色
|
||||
4. 红色节点的子节点是黑色
|
||||
5. 所有路径的黑色节点数量相同
|
||||
|
||||
```java
|
||||
// 验证红黑树正确性的方法
|
||||
public boolean isRedBlackTree(RedBlackNode<T> node) {
|
||||
if (node == NIL) return true;
|
||||
|
||||
// 验证颜色
|
||||
if (node.color != Color.RED && node.color != Color.BLACK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证红色节点的子节点
|
||||
if (node.color == Color.RED) {
|
||||
if (node.left.color == Color.RED || node.right.color == Color.RED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证路径长度
|
||||
int leftBlackHeight = getBlackHeight(node.left);
|
||||
int rightBlackHeight = getBlackHeight(node.right);
|
||||
if (leftBlackHeight != rightBlackHeight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 递归验证子树
|
||||
return isRedBlackTree(node.left) && isRedBlackTree(node.right);
|
||||
}
|
||||
|
||||
// 计算黑色高度
|
||||
private int getBlackHeight(RedBlackNode<T> node) {
|
||||
int height = 0;
|
||||
while (node != NIL) {
|
||||
if (node.color == Color.BLACK) {
|
||||
height++;
|
||||
}
|
||||
node = node.left;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
```
|
||||
|
||||
### Q6: 红黑树如何处理大规模数据?
|
||||
**答**:
|
||||
对于大规模数据,红黑树的处理策略:
|
||||
1. **内存考虑**:大规模数据可能导致内存问题
|
||||
2. **性能优化**:
|
||||
- 批量插入时可以暂时忽略平衡
|
||||
- 使用延迟修复策略
|
||||
3. **替代方案**:
|
||||
- 对于磁盘数据,使用 B 树或 B+ 树
|
||||
- 对于分布式数据,使用分布式数据结构
|
||||
|
||||
### Q7: 红黑树的删除操作有哪些情况?
|
||||
**答**:
|
||||
删除操作主要处理以下情况:
|
||||
1. **删除红色节点**:直接删除,不影响平衡
|
||||
2. **删除黑色节点**:需要修复,分为多种情况
|
||||
- 替代节点是红色:着色为黑色
|
||||
- 替代节点是黑色:
|
||||
- 兄弟节点是红色
|
||||
- 兄弟节点是黑色,且子节点都是黑色
|
||||
- 兄弟节点是黑色,且有一个红色子节点
|
||||
650
questions/08-算法与数据结构/跳表原理.md
Normal file
650
questions/08-算法与数据结构/跳表原理.md
Normal file
@@ -0,0 +1,650 @@
|
||||
# 跳表 (Skip List)
|
||||
|
||||
## 数据结构原理
|
||||
|
||||
### 什么是跳表?
|
||||
跳表是一种概率性的数据结构,通过在多个层级上维护有序的链表来提供高效的查找、插入和删除操作。它是一种在平衡二叉搜索树和链表之间的折中方案,实现简单且性能优异。
|
||||
|
||||
### 跳表的核心概念
|
||||
|
||||
1. **层级**:跳表由多个层级组成,最高层是稀疏的,最低层是稠密的
|
||||
2. **节点**:每个节点在不同层级中有多个指针
|
||||
3. **索引**:高层级作为低层级的索引,快速定位
|
||||
4. **概率性平衡**:通过随机算法保证树的平衡性
|
||||
|
||||
### 跳表的工作原理
|
||||
|
||||
1. **查找**:从最高层开始,向右查找,无法向右时向下继续
|
||||
2. **插入**:随机决定插入的层级,在相应层级插入节点
|
||||
3. **删除**:在所有层级中删除对应节点
|
||||
4. **平衡**:通过随机概率保持树的平衡性
|
||||
|
||||
## 图解说明
|
||||
|
||||
```
|
||||
跳表结构示例(最大层级 4):
|
||||
|
||||
Level 3: ---1---10---40---70---
|
||||
Level 2: ---1-----10-----70---
|
||||
Level 1: ---1------10-----70---
|
||||
Level 0: 1->2->3->4->5->6->7->8->9->10->11->...->70
|
||||
|
||||
查找过程(查找 7):
|
||||
- Level 3: 从 1 开始,无法向右,向下到 Level 2
|
||||
- Level 2: 从 1 开始,无法向右,向下到 Level 1
|
||||
- Level 1: 从 1 开始,无法向右,向下到 Level 0
|
||||
- Level 0: 从 1 开始,遍历到 7
|
||||
```
|
||||
|
||||
### 跂表节点结构
|
||||
|
||||
```
|
||||
SkipListNode {
|
||||
value: 7
|
||||
next[0] -> 8
|
||||
next[1] -> 10
|
||||
next[2] -> 10
|
||||
next[3] -> 40
|
||||
}
|
||||
```
|
||||
|
||||
### 层级选择算法
|
||||
|
||||
```java
|
||||
// 随机生成层级
|
||||
int level = 0;
|
||||
while (random() < 0.5 && level < MAX_LEVEL) {
|
||||
level++;
|
||||
}
|
||||
```
|
||||
|
||||
## Java 代码实现
|
||||
|
||||
### 节点类定义
|
||||
|
||||
```java
|
||||
class SkipListNode<T extends Comparable<T>> {
|
||||
T value;
|
||||
SkipListNode<T>[] next;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public SkipListNode(T value, int level) {
|
||||
this.value = value;
|
||||
this.next = new SkipListNode[level + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 跳表实现
|
||||
|
||||
```java
|
||||
import java.util.Random;
|
||||
|
||||
public class SkipList<T extends Comparable<T>> {
|
||||
private static final int MAX_LEVEL = 16;
|
||||
private static final double PROBABILITY = 0.5;
|
||||
|
||||
private SkipListNode<T> header;
|
||||
private int level;
|
||||
private int size;
|
||||
private Random random;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public SkipList() {
|
||||
this.header = new SkipListNode<>(null, MAX_LEVEL);
|
||||
this.level = 0;
|
||||
this.size = 0;
|
||||
this.random = new Random();
|
||||
}
|
||||
|
||||
// 随机生成层级
|
||||
private int randomLevel() {
|
||||
int lvl = 0;
|
||||
while (lvl < MAX_LEVEL && random.nextDouble() < PROBABILITY) {
|
||||
lvl++;
|
||||
}
|
||||
return lvl;
|
||||
}
|
||||
|
||||
// 插入操作
|
||||
public void insert(T value) {
|
||||
SkipListNode<T>[] update = new SkipListNode[MAX_LEVEL + 1];
|
||||
SkipListNode<T> current = header;
|
||||
|
||||
// 从最高层开始查找插入位置
|
||||
for (int i = level; i >= 0; i--) {
|
||||
while (current.next[i] != null &&
|
||||
current.next[i].value.compareTo(value) < 0) {
|
||||
current = current.next[i];
|
||||
}
|
||||
update[i] = current;
|
||||
}
|
||||
|
||||
// 确定新节点的层级
|
||||
int newLevel = randomLevel();
|
||||
|
||||
// 如果新层级大于当前层级,更新高层级的指针
|
||||
if (newLevel > level) {
|
||||
for (int i = level + 1; i <= newLevel; i++) {
|
||||
update[i] = header;
|
||||
}
|
||||
level = newLevel;
|
||||
}
|
||||
|
||||
// 创建新节点并插入
|
||||
SkipListNode<T> newNode = new SkipListNode<>(value, newLevel);
|
||||
for (int i = 0; i <= newLevel; i++) {
|
||||
newNode.next[i] = update[i].next[i];
|
||||
update[i].next[i] = newNode;
|
||||
}
|
||||
|
||||
size++;
|
||||
}
|
||||
|
||||
// 查找操作
|
||||
public boolean contains(T value) {
|
||||
SkipListNode<T> current = header;
|
||||
|
||||
for (int i = level; i >= 0; i--) {
|
||||
while (current.next[i] != null &&
|
||||
current.next[i].value.compareTo(value) < 0) {
|
||||
current = current.next[i];
|
||||
}
|
||||
}
|
||||
|
||||
current = current.next[0];
|
||||
return current != null && current.value.compareTo(value) == 0;
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
public void delete(T value) {
|
||||
SkipListNode<T>[] update = new SkipListNode[MAX_LEVEL + 1];
|
||||
SkipListNode<T> current = header;
|
||||
|
||||
// 查找要删除的节点
|
||||
for (int i = level; i >= 0; i--) {
|
||||
while (current.next[i] != null &&
|
||||
current.next[i].value.compareTo(value) < 0) {
|
||||
current = current.next[i];
|
||||
}
|
||||
update[i] = current;
|
||||
}
|
||||
|
||||
current = current.next[0];
|
||||
if (current != null && current.value.compareTo(value) == 0) {
|
||||
// 从所有层级中删除
|
||||
for (int i = 0; i <= level; i++) {
|
||||
if (update[i].next[i] != current) {
|
||||
break;
|
||||
}
|
||||
update[i].next[i] = current.next[i];
|
||||
}
|
||||
|
||||
// 更新当前层级
|
||||
while (level > 0 && header.next[level] == null) {
|
||||
level--;
|
||||
}
|
||||
|
||||
size--;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取最小值
|
||||
public T getMin() {
|
||||
SkipListNode<T> current = header.next[0];
|
||||
return current != null ? current.value : null;
|
||||
}
|
||||
|
||||
// 获取最大值
|
||||
public T getMax() {
|
||||
SkipListNode<T> current = header;
|
||||
for (int i = level; i >= 0; i--) {
|
||||
while (current.next[i] != null) {
|
||||
current = current.next[i];
|
||||
}
|
||||
}
|
||||
return current.value;
|
||||
}
|
||||
|
||||
// 跳表大小
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
// 是否为空
|
||||
public boolean isEmpty() {
|
||||
return size == 0;
|
||||
}
|
||||
|
||||
// 打印跳表结构
|
||||
public void printList() {
|
||||
for (int i = level; i >= 0; i--) {
|
||||
SkipListNode<T> node = header.next[i];
|
||||
System.out.print("Level " + i + ": ");
|
||||
while (node != null) {
|
||||
System.out.print(node.value + " ");
|
||||
node = node.next[i];
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
|
||||
// 中序遍历
|
||||
public void traverse() {
|
||||
SkipListNode<T> current = header.next[0];
|
||||
while (current != null) {
|
||||
System.out.print(current.value + " ");
|
||||
current = current.next[0];
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完整的实现(包括范围查询)
|
||||
|
||||
```java
|
||||
// 跳表完整实现
|
||||
public class EnhancedSkipList<T extends Comparable<T>> {
|
||||
private static final int MAX_LEVEL = 16;
|
||||
private static final double PROBABILITY = 0.5;
|
||||
|
||||
private static class Node<T> {
|
||||
T value;
|
||||
Node<T>[] next;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Node(T value, int level) {
|
||||
this.value = value;
|
||||
this.next = new Node[level + 1];
|
||||
}
|
||||
}
|
||||
|
||||
private Node<T> header;
|
||||
private int level;
|
||||
private int size;
|
||||
private Random random;
|
||||
|
||||
public EnhancedSkipList() {
|
||||
this.header = new Node<>(null, MAX_LEVEL);
|
||||
this.level = 0;
|
||||
this.size = 0;
|
||||
this.random = new Random();
|
||||
}
|
||||
|
||||
// 范围查询
|
||||
public List<T> rangeQuery(T start, T end) {
|
||||
List<T> result = new ArrayList<>();
|
||||
if (start.compareTo(end) > 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Node<T> current = header;
|
||||
|
||||
// 找到 start 的位置
|
||||
for (int i = level; i >= 0; i--) {
|
||||
while (current.next[i] != null &&
|
||||
current.next[i].value.compareTo(start) < 0) {
|
||||
current = current.next[i];
|
||||
}
|
||||
}
|
||||
|
||||
current = current.next[0];
|
||||
while (current != null && current.value.compareTo(end) <= 0) {
|
||||
result.add(current.value);
|
||||
current = current.next[0];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 获取前驱节点
|
||||
public T predecessor(T value) {
|
||||
Node<T> current = header;
|
||||
T predecessor = null;
|
||||
|
||||
for (int i = level; i >= 0; i--) {
|
||||
while (current.next[i] != null &&
|
||||
current.next[i].value.compareTo(value) < 0) {
|
||||
current = current.next[i];
|
||||
if (i == 0) {
|
||||
predecessor = current.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return predecessor;
|
||||
}
|
||||
|
||||
// 获取后继节点
|
||||
public T successor(T value) {
|
||||
if (!contains(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Node<T> current = header;
|
||||
|
||||
for (int i = level; i >= 0; i--) {
|
||||
while (current.next[i] != null &&
|
||||
current.next[i].value.compareTo(value) <= 0) {
|
||||
current = current.next[i];
|
||||
}
|
||||
}
|
||||
|
||||
current = current.next[0];
|
||||
return current != null ? current.value : null;
|
||||
}
|
||||
|
||||
// 统计小于某值的元素个数
|
||||
public int countLessThan(T value) {
|
||||
Node<T> current = header;
|
||||
int count = 0;
|
||||
|
||||
for (int i = level; i >= 0; i--) {
|
||||
while (current.next[i] != null &&
|
||||
current.next[i].value.compareTo(value) < 0) {
|
||||
count += Math.pow(2, i); // 近似计算
|
||||
current = current.next[i];
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
public SkipListStats getStats() {
|
||||
SkipListStats stats = new SkipListStats();
|
||||
stats.size = size;
|
||||
stats.height = level + 1;
|
||||
|
||||
// 计算每个层级的节点数
|
||||
int[] levelCounts = new int[MAX_LEVEL + 1];
|
||||
Node<T> current = header;
|
||||
|
||||
for (int i = 0; i <= level; i++) {
|
||||
levelCounts[i] = 0;
|
||||
}
|
||||
|
||||
current = header.next[0];
|
||||
while (current != null) {
|
||||
for (int i = 0; i <= level && i <= current.next.length - 1; i++) {
|
||||
levelCounts[i]++;
|
||||
}
|
||||
current = current.next[0];
|
||||
}
|
||||
|
||||
stats.levelCounts = levelCounts;
|
||||
return stats;
|
||||
}
|
||||
|
||||
public static class SkipListStats {
|
||||
public int size;
|
||||
public int height;
|
||||
public int[] levelCounts;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 时间复杂度分析
|
||||
|
||||
### 操作时间复杂度
|
||||
|
||||
| 操作 | 时间复杂度 | 说明 |
|
||||
|------|------------|------|
|
||||
| 查找 | O(log n) | 最多遍历 log n 层 |
|
||||
| 插入 | O(log n) | 查找位置 + 随机决定层级 |
|
||||
| 删除 | O(log n) | 查找节点 + 从所有层级删除 |
|
||||
| 范围查询 | O(log n + k) | k 是结果集大小 |
|
||||
| 最值查找 | O(1) | 直接访问首尾节点 |
|
||||
|
||||
### 空间复杂度
|
||||
|
||||
- O(n log n) - 每个节点平均存在 log n 层
|
||||
- 需要额外空间维护多层级指针
|
||||
|
||||
### 概率分析
|
||||
|
||||
1. **期望层级**:对于 n 个元素,期望层级为 log n
|
||||
2. **期望指针数量**:每个节点期望有 2 个指针
|
||||
3. **查找效率**:O(log n) 概率保证
|
||||
|
||||
## 实际应用场景
|
||||
|
||||
### 1. Redis 有序集合
|
||||
- **zset 实现**:Redis 的有序集合使用跳表实现
|
||||
- **范围查询**:支持高效的区间查询
|
||||
- **分数排序**:基于分数进行排序
|
||||
|
||||
```java
|
||||
// Redis 有序集合模拟
|
||||
public class RedisSortedSet {
|
||||
private EnhancedSkipList<Double, String> skipList;
|
||||
|
||||
public void add(double score, String member) {
|
||||
skipList.insert(score, member);
|
||||
}
|
||||
|
||||
public List<String> rangeByScore(double min, double max) {
|
||||
return skipList.rangeQuery(min, max);
|
||||
}
|
||||
|
||||
public boolean contains(String member) {
|
||||
return skipList.contains(member);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 数据库索引
|
||||
- **内存索引**:用于内存数据库的索引
|
||||
- **范围查询**:支持高效的范围查找
|
||||
- **插入性能**:比 B 树实现简单
|
||||
|
||||
```java
|
||||
// 数据库索引实现
|
||||
public class DatabaseIndex {
|
||||
private EnhancedSkipList<Object, Row> index;
|
||||
|
||||
public void insert(Object key, Row row) {
|
||||
index.insert(key, row);
|
||||
}
|
||||
|
||||
public List<Row> rangeQuery(Object start, Object end) {
|
||||
return index.rangeQuery(start, end);
|
||||
}
|
||||
|
||||
public Row find(Object key) {
|
||||
return index.find(key);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 网络路由
|
||||
- **路由表**:IP 地址范围查找
|
||||
- **ACL 控制**:访问控制列表匹配
|
||||
|
||||
```java
|
||||
// 路由表实现
|
||||
public class RoutingTable {
|
||||
private EnhancedSkipList<String, Route> routes;
|
||||
|
||||
public Route findRoute(String ip) {
|
||||
return routes.find(ip);
|
||||
}
|
||||
|
||||
public List<Route> findRoutesInSubnet(String subnet) {
|
||||
return routes.rangeQuery(subnet, subnet + "255");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 缓存系统
|
||||
- **多级缓存**:实现分层缓存
|
||||
- **缓存查找**:快速定位缓存项
|
||||
|
||||
```java
|
||||
// 多级缓存实现
|
||||
public class MultiLevelCache {
|
||||
private EnhancedSkipList<String, Object> l1Cache;
|
||||
private EnhancedSkipList<String, Object> l2Cache;
|
||||
|
||||
public Object get(String key) {
|
||||
Object value = l1Cache.find(key);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
value = l2Cache.find(key);
|
||||
if (value != null) {
|
||||
l1Cache.insert(key, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 与其他数据结构的对比
|
||||
|
||||
| 数据结构 | 查找时间 | 插入时间 | 删除时间 | 适用场景 |
|
||||
|----------|----------|----------|----------|----------|
|
||||
| 跳表 | O(log n) | O(log n) | O(log n) | 内存数据、范围查询 |
|
||||
| 平衡二叉树 | O(log n) | O(log n) | O(log n) | 内存数据、查找密集 |
|
||||
| 哈希表 | O(1) | O(1) | O(1) | 精确查找、内存数据 |
|
||||
| B 树 | O(log n) | O(log n) | O(log n) | 磁盘存储、索引 |
|
||||
| 数组 | O(n) | O(n) | O(n) | 小规模、有序数据 |
|
||||
|
||||
### 跳表的优势
|
||||
|
||||
1. **实现简单**:相比平衡二叉树,实现更简单
|
||||
2. **并发友好**:部分操作可以并发执行
|
||||
3. **内存效率**:空间使用比平衡树更合理
|
||||
4. **概率平衡**:不需要复杂的旋转操作
|
||||
5. **支持范围查询**:链表结构天然支持范围操作
|
||||
|
||||
### 跳表的劣势
|
||||
|
||||
1. **内存使用**:比普通链表使用更多内存
|
||||
2. **最坏情况**:概率性数据结构,最坏情况较差
|
||||
3. **常数因子**:比平衡树的常数因子大
|
||||
|
||||
## 常见面试问题
|
||||
|
||||
### Q1: 跳表和平衡二叉树有什么区别?
|
||||
**答**:
|
||||
**主要区别**:
|
||||
1. **实现复杂度**:跳表实现简单,平衡树需要复杂的旋转操作
|
||||
2. **内存使用**:跳表使用更多内存(多层级指针),平衡树内存使用更紧凑
|
||||
3. **并发性能**:跳表某些操作更容易并发执行
|
||||
4. **平衡机制**:跳表是概率性平衡,平衡树是确定性平衡
|
||||
5. **查找性能**:平衡树常数因子更好,跳表略差
|
||||
|
||||
### Q2: 跳表的最大层级如何确定?为什么?
|
||||
**答**:
|
||||
最大层级的确定:
|
||||
1. **经验值**:通常设置为 16-32,足够处理大多数情况
|
||||
2. **概率保证**:对于 n 个元素,期望层级为 log n
|
||||
3. **空间考虑**:最大层级决定最坏情况的内存使用
|
||||
4. **性能平衡**:太高浪费内存,太低影响性能
|
||||
|
||||
### Q3: 跳表的时间复杂度如何证明?
|
||||
**答**:
|
||||
时间复杂度分析:
|
||||
1. **查找分析**:每层需要遍历的节点数逐渐减少
|
||||
2. **几何级数**:每层节点数呈几何级数减少
|
||||
3. **期望层数**:每个节点的期望层数为 2
|
||||
4. **总查找步数**:期望查找步数为 O(log n)
|
||||
|
||||
### Q4: Redis 为什么选择跳表实现有序集合?
|
||||
**答**:
|
||||
选择跳表的原因:
|
||||
1. **实现简单**:相比平衡树更容易实现
|
||||
2. **内存效率**:相比平衡树内存使用更合理
|
||||
3. **并发性能**:跳表某些操作可以并发执行
|
||||
4. **范围查询**:天然支持范围查询操作
|
||||
5. **性能足够**:对于大多数场景性能足够好
|
||||
|
||||
### Q5: 跳表如何保证查找效率?
|
||||
**答**:
|
||||
保证效率的关键:
|
||||
1. **层级设计**:高层级作为低层级的索引
|
||||
2. **概率平衡**:通过随机算法保证树的平衡性
|
||||
3. **快速定位**:从高层级快速定位到大致位置
|
||||
4. **层数控制**:每个节点只存在于适当数量的层级中
|
||||
|
||||
### Q6: 如何优化跳表的内存使用?
|
||||
**答**:
|
||||
内存优化策略:
|
||||
1. **动态层级**:根据实际数据动态调整最大层级
|
||||
2. **压缩层级**:合并过空的层级
|
||||
3. **节点复用**:复用不再使用的节点
|
||||
4. **缓存友好**:优化内存布局,提高缓存命中率
|
||||
5. **惰性删除**:延迟删除,减少内存碎片
|
||||
|
||||
### Q7: 跳表在并发环境下如何处理?
|
||||
**答**:
|
||||
并发处理方案:
|
||||
1. **细粒度锁**:对不同层使用不同的锁
|
||||
2. **无锁设计**:使用 CAS 操作实现无锁跳表
|
||||
3. **版本控制**:使用版本号实现乐观并发控制
|
||||
4. **分段锁**:将跳表分段,每段独立加锁
|
||||
5. **读无锁**:读操作不加锁,写操作加锁
|
||||
|
||||
```java
|
||||
// 并发跳表简化实现
|
||||
public class ConcurrentSkipList<T extends Comparable<T>> {
|
||||
private final ReentrantLock[] locks;
|
||||
private final int lockCount;
|
||||
|
||||
public ConcurrentSkipList() {
|
||||
this.lockCount = 16;
|
||||
this.locks = new ReentrantLock[lockCount];
|
||||
for (int i = 0; i < lockCount; i++) {
|
||||
locks[i] = new ReentrantLock();
|
||||
}
|
||||
}
|
||||
|
||||
private int getLockIndex(T value) {
|
||||
return Math.abs(value.hashCode() % lockCount);
|
||||
}
|
||||
|
||||
public void insert(T value) {
|
||||
int lockIndex = getLockIndex(value);
|
||||
locks[lockIndex].lock();
|
||||
try {
|
||||
// 插入逻辑
|
||||
} finally {
|
||||
locks[lockIndex].unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Q8: 跳表的性能如何随数据量变化?
|
||||
**答**:
|
||||
性能变化规律:
|
||||
1. **时间复杂度**:保持 O(log n) 不变
|
||||
2. **空间复杂度**:随数据量线性增长
|
||||
3. **常数因子**:数据量越大,常数因子影响越小
|
||||
4. **缓存影响**:数据量较大时,缓存命中率下降
|
||||
5. **内存压力**:大数据量时内存使用成为瓶颈
|
||||
|
||||
### Q9: 如何处理跳表中的重复数据?
|
||||
**答**:
|
||||
重复数据处理:
|
||||
1. **允许重复**:修改插入逻辑,允许相同值存在
|
||||
2. **去重处理**:在插入时检查是否已存在
|
||||
3. **多值节点**:在节点中存储值的集合
|
||||
4. **计数器**:在节点中增加计数器
|
||||
5. **策略选择**:根据业务需求选择合适的处理方式
|
||||
|
||||
### Q10: 跳表和 B 树有什么相似之处?
|
||||
**答**:
|
||||
相似之处:
|
||||
1. **分层结构**:都是多层结构,高层级作为索引
|
||||
2. **查找效率**:都是 O(log n) 时间复杂度
|
||||
3. **范围查询**:都支持高效的范围查询
|
||||
4. **平衡性**:都维护数据的平衡性
|
||||
5. **空间局部性**:都考虑数据的局部性原理
|
||||
|
||||
**主要区别**:
|
||||
- 跳表是基于链表的概率结构
|
||||
- B 树是基于数组块的确定性结构
|
||||
- 跳表适用于内存,B 树适用于磁盘
|
||||
Reference in New Issue
Block a user