题目:带 WriteBuffer 的内存读写操作
背景介绍
想象一下,你是一位非常繁忙的大公司 CEO(中央处理器 CPU),你的工作是飞速地处理各种事务。而公司的中央档案库(主存 data
)虽然巨大且可靠,但管理员(内存控制器)的动作很慢。
如果你每做一个小决定(比如修改一份文件的某一个字节),都要亲自跑到档案库,等待管理员找到文件、修改、再归档,那你宝贵的时间就都浪费在等待上了。这显然无法忍受。
为了解决这个问题,你聘请了一位行动非常迅速的秘书,并给了他/她一个很小的待办事项记事本。这个记事本就是 WriteBuffer
(写缓存) 。
现在,我们来认识一下这个系统的三个核心部分:
-
你 (CPU): 负责发出指令,是整个系统的决策者。
-
中央档案库 (
data
): 这是最终存储所有信息的地方,巨大、永久但访问速度慢。它在我们的问题中由一长串十六进制字符串表示。 -
你的秘书和记事本 (
WriteBuffer
):- 记事本 (
writeBuffer
) : 这是一个先进先出 (FIFO) 的队列,就像一个按顺序排列的待办事项列表。 - 容量有限 (
bufferCap
) : 记事本上只能写下固定数量(例如bufferCap
条)的待办事项。如果写满了,就必须先处理掉最老的一条,才能写新的。
- 记事本 (
你每天会处理三种不同的工作:
场景一:写入新数据 (指令 op = 1)
你现在有一个大的修改任务,比如"把公司手册第 2 页到第 12 页的内容,全部改成'高效工作'"。
- 指令格式:
[1, 起始地址, 长度, 内容]
,例如[1, 2, 11, 120]
。
你不会亲自去档案库改。你会把这个任务交给你的秘书,规则如下:
-
任务分解: 秘书很聪明,他知道一次性处理太长的内容效率不高。他会把你的大任务拆分成多个标准大小(最多8个字节)的"小纸条"(写请求)。
- 例如,修改 11 个字节的任务,会被拆成一张"修改 8 字节"和一张"修改 3 字节"的纸条。
-
放入待办列表: 秘书会把这些小纸条依次 贴到记事本(
writeBuffer
)的末尾。 -
记事本满了怎么办? 在贴一张新纸条之前 ,如果秘书发现记事本已经满了(达到了
bufferCap
的上限),他会立即处理记事本上最早(最上面)的那条待办事项------也就是亲自去档案库完成那项修改。处理完后,撕掉那张旧纸条,再把新的贴上去。
场景二:读取数据 (指令 op = 2)
你需要一份资料,比如"把公司手册第 11 页的内容拿给我看看"。
- 指令格式:
[2, 起始地址, 长度, 0]
在你读取之前,你必须确保你读到的是最新版本的信息,而不是档案库里可能已经过时的旧版本。
-
数据一致性问题: 你问秘书:"我马上要看第 11 页,你的记事本上有没有还没处理的、关于第 11 页或附近页面的修改?"
-
刷新缓存的规则:
- 秘书会检查他记事本上所有的待办修改。如果发现任何一条修改的范围和你想要读取的范围有重叠,问题就来了。
- 为了保证你读到的是绝对正确的信息,秘书必须严格按照待办事项的先后顺序(FIFO) ,从最早的一条开始,一直处理到最后一个与你读取范围相关的修改。他不能跳过中间的任何一条。
- 例如,记事本上有三条待办:
①修改第5页
,②修改第12页
,③修改第8页
。如果你要读第 12 页,那么秘书必须把①
和②
都处理完(去档案库修改),才能让你去读。③
因为在②
之后,暂时可以不处理。
场景三:手动同步 (指令 op = 3)
到了下班时间,或者你需要确保所有修改都已生效。
- 指令格式:
[3, 0, 0, 0]
- 意图: 你告诉秘书:"清空所有待办事项,把记事本上所有的修改都去档案库里落实了。"
- 执行规则: 秘书会从记事本的最早一条开始,逐一处理,直到记事本被清空。
现在,请你来扮演这位秘书的角色,实现这个 WriteBuffer
系统。
给定一个初始的档案库状态 data
,一个记事本容量 bufferCap
,以及一系列按顺序下达的操作指令 operations
。
你需要精确地模拟上述所有过程,并最终返回操作完成后,中央档案库 data
的最终状态(以大写十六进制字符串的形式)。
核心组件
- 主存
data
: 系统的基础存储,初始内容由一个十六进制字符串给出。 - 写缓存
WriteBuffer
: 一个 FIFO 队列,用于暂存待写入主存的"写请求"。其容量上限为bufferCap
。 - 操作
operations
: 一系列需要按顺序执行的内存操作指令。
功能要求
你需要实现一个系统,根据给定的初始主存 data
和缓存容量 bufferCap
,依次处理 operations
列表中的所有操作,并最终返回操作完成后主存 data
的内容。
内存操作指令 operations[i] = [op, para1, para2, para3]
详解:
1. 内存写 (op = 1)
-
指令格式:
[1, 偏移para1, 长度para2, 内容para3]
-
意图: 在主存
data
中,从偏移para1
的地址开始,将连续长度para2
个字节的内容都写为内容para3
。 -
执行规则:
-
请求分解: 该写操作会按每 8 字节分解为多个小的写请求(最后一个请求可能不足 8 字节)。
-
入队与刷新: 所有分解后的写请求按地址从小到大依次放入缓存队列的末尾。
- 在每放入一个小的写请求之前 ,如果缓存已满(即缓存中的请求数达到了
bufferCap
),则必须先从缓存队首取出一个最早的写请求,将其内容写入主存(这个过程称为"刷新"或 "Flush")。 - 刷新后,再将新的小请求放入缓存队尾。
- 在每放入一个小的写请求之前 ,如果缓存已满(即缓存中的请求数达到了
-
2. 内存读 (op = 2)
-
指令格式:
[2, 偏移para1, 长度para2, 0]
(para3 无意义) -
意图: 从主存
data
的偏移para1
地址开始,连续读出长度para2
字节的数据。 -
执行规则:
- 检查缓存: 检查要读取的数据范围
[para1, para1 + para2)
是否与缓存中任何一个写请求的范围有重叠。 - 刷新策略: 如果存在重叠,则需要按 FIFO 顺序 将缓存中所有 与该读请求重叠的、以及在该请求之前的所有写请求,全部从缓存中取出并写入主存。
- 注意: 不能从缓存中间直接取出写请求。必须从队首开始,依次刷新,直到最后一个与读请求重叠的请求被刷新为止。
- 检查缓存: 检查要读取的数据范围
3. 内存同步 (op = 3)
- 指令格式:
[3, 0, 0, 0]
(para1, para2, para3 无意义) - 意图: 将缓存中的所有写请求都取出来并写入主存,清空缓存。
输入格式
-
第一个参数
bufferCap
:- 表示 WriteBuffer 的容量(最多能缓存的请求个数)。
4 <= bufferCap <= 32
-
第二个参数
operations
:- 一个二维数组,包含一系列操作指令。
1 <= operations.length <= 1024
1 <= op <= 3
para1
在data
地址范围之内。0 <= para2 <= bufferCap * 8
(当op
为读或写时,para2 >= 1
)。0 <= para3 <= 255
-
第三个参数
data
:- 一个表示主存初始内容的十六进制字符串(字符为
[0-9A-F]
,字母为大写)。 - 每两个字符表示一个字节。
2 <= data.length <= 30000
,且data.length
为 2 的整数倍。
- 一个表示主存初始内容的十六进制字符串(字符为
用例保证读写地址都在 data
范围之内。
输出格式
- 一个字符串,表示经过所有内存操作后,主存
data
中的数据内容(大写十六进制字符串)。
样例
输入样例 1
lua
4
[[1, 1, 3, 255], [3, 0, 0, 0], [1, 2, 11, 120], [2, 11, 1, 0]]
"1DA0820000000000000901ABCDEF00"
输出样例 1
arduino
"1DFF7878787878787878787878EF00"
样例 1 解释
-
[1, 1, 3, 255]
: 写操作。分解为 1 个写请求[offset=1, len=3, val=0xFF]
。缓存未满,直接入队。- 缓存状态:
{[1,3,255]}
- 主存: (不变)
1DA082...
- 缓存状态:
-
[3, 0, 0, 0]
: 同步操作。- 将缓存中所有请求写入主存。
- 缓存状态:
[]
(空) - 主存:
1DFFFFFF0000...
-
[1, 2, 11, 120]
: 写操作。length=11
被分解为两个写请求:- 请求1:
[offset=2, len=8, val=0x78]
- 请求2:
[offset=10, len=3, val=0x78]
- 依次入队。
- 缓存状态:
{[2,8,120], [10,3,120]}
- 主存: (不变)
1DFFFFFF...
- 请求1:
-
[2, 11, 1, 0]
: 读操作。读取范围是[11, 12)
。- 检查缓存,发现读范围与请求
[10,3,120]
(实际范围[10,13)
)重叠。 - 按 FIFO 规则,需要将此请求及其之前的所有请求都刷新到主存。
- 刷新请求
[2,8,120]
-> 主存1DFF787878787878...
- 刷新请求
[10,3,120]
-> 主存...787878EF00
- 缓存状态:
[]
(空) - 主存:
1DFF7878787878787878787878EF00
- 检查缓存,发现读范围与请求
最终返回该主存状态。
java
import java.util.*;
// 使用 Java Record (需要 JDK 16+) 来简洁地表示一个写请求,它会自动生成构造函数、getter等。
// 如果使用旧版 Java,可以替换为一个普通的 final class。
record WriteRequest(int offset, int length, int value) {}
/**
* 模拟带 WriteBuffer 的内存读写操作的系统。
*/
class WriteBufferSystem {
private final int bufferCap; // 缓存请求个数的上限
private final byte[] mainMemory; // 主存,用字节数组表示
private final Queue<WriteRequest> writeBuffer; // 写缓存,使用队列实现 FIFO
/**
* 构造函数 - 初始化系统
* @param bufferCap 缓存容量
* @param initialData 主存的初始数据(十六进制字符串)
*/
public WriteBufferSystem(int bufferCap, String initialData) {
this.bufferCap = bufferCap;
// 使用 LinkedList,因为它既是 Queue (FIFO),也方便迭代
this.writeBuffer = new LinkedList<>();
// 将输入的十六进制字符串转换为字节数组作为主存
this.mainMemory = hexStringToByteArray(initialData);
}
/**
* op = 1: 内存写操作
*/
public void write(int offset, int length, int value) {
int currentOffset = offset;
int remainingLength = length;
// 按每 8 字节分解为多个写请求
while (remainingLength > 0) {
int chunkLength = Math.min(8, remainingLength);
// 如果缓存已满,则按 FIFO 取出最早的请求并写入主存
if (writeBuffer.size() >= this.bufferCap) {
flushOneRequest();
}
// 将新的写请求放入缓存
writeBuffer.offer(new WriteRequest(currentOffset, chunkLength, value));
// 更新下一次分解的偏移和长度
currentOffset += chunkLength;
remainingLength -= chunkLength;
}
}
/**
* op = 2: 内存读操作
*/
public void read(int offset, int length) {
int readStart = offset;
int readEnd = offset + length - 1;
int lastOverlappingIndex = -1; // 记录最后一个与读区域重叠的缓存请求的索引
int currentIndex = 0;
// 遍历缓存,找到最后一个重叠的请求
for (WriteRequest req : writeBuffer) {
int reqStart = req.offset();
int reqEnd = req.offset() + req.length() - 1;
// 判断区间是否重叠: max(start1, start2) <= min(end1, end2)
if (Math.max(readStart, reqStart) <= Math.min(readEnd, reqEnd)) {
lastOverlappingIndex = currentIndex;
}
currentIndex++;
}
// 如果找到了重叠的请求,则将该请求及之前的所有请求都写入主存
if (lastOverlappingIndex != -1) {
for (int i = 0; i <= lastOverlappingIndex; i++) {
flushOneRequest();
}
}
// 题目不要求返回读取的数据,只需模拟刷新缓存的行为
}
/**
* op = 3: 内存同步操作
*/
public void sync() {
// 将缓存中的所有写请求都写入主存
while (!writeBuffer.isEmpty()) {
flushOneRequest();
}
}
/**
* 获取最终的主存状态
* @return 十六进制字符串形式的主存内容
*/
public String getFinalMemoryState() {
// 在返回最终结果前,确保所有缓存都已同步
sync();
return byteArrayToHexString(this.mainMemory);
}
// --- 私有辅助方法 ---
/**
* 从缓存中取出最早的一个写请求并写入主存。
*/
private void flushOneRequest() {
WriteRequest requestToFlush = writeBuffer.poll(); // poll() 获取并移除队首元素
if (requestToFlush != null) {
// 将请求的内容写入主存的相应位置
for (int i = 0; i < requestToFlush.length(); i++) {
// 注意写请求是对每个位置都写同样的内容
mainMemory[requestToFlush.offset() + i] = (byte) requestToFlush.value();
}
}
}
/**
* 辅助方法:将十六进制字符串转换为字节数组。
* 这个方法的核心目标是将一个表示十六进制数据的字符串(例如 `"1FA0"`)转换成它实际代表的字节数组(例如 `[0x1F, 0xA0]`)。
* 十六进制字符 (Hex Char): 0-9, A-F,共 16 个值。因为 `16 = 2^4`,所以一个十六进制字符正好能表示一个 4 位的二进制数(也称为"半字节"或"Nibble")。
* 例如, 十六进制的 `'F'` 等于十进制的 `15`,等于二进制的 `1111`。
* 因此,每两个十六进制字符就可以组合成一个完整的 8 位字节。
*/
private static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
// 注意 i 每次增加 2
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
return data;
}
/**
* 辅助方法:将字节数组转换为大写的十六进制字符串。
*/
private static String byteArrayToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
// %02X 表示输出为两位大写的十六进制数,不足两位时前面补0
sb.append(String.format("%02X", b));
}
return sb.toString();
}
}
public class Main {
public static void main(String[] args) {
// 模拟样例1
System.out.println("--- 样例 1 ---");
int bufferCap1 = 4;
int[][] operations1 = {{1, 1, 3, 255}, {3, 0, 0, 0}, {1, 2, 11, 120}, {2, 11, 1, 0}};
String data1 = "1DA0820000000000000901ABCDEF00";
runTest(bufferCap1, operations1, data1);
// 预期输出: "1DFF7878787878787878787878EF00"
System.out.println();
// 模拟样例2
System.out.println("--- 样例 2 ---");
int bufferCap2 = 5;
int[][] operations2 = {{1, 35, 2, 100}, {1, 0, 40, 255}, {1, 11, 10, 81}, {1, 16, 12, 173}, {2, 16, 3, 0}, {2, 0, 3, 0}};
String data2 = "00000000000000000000000000000000000000000000000000000000000000000000000000000000";
runTest(bufferCap2, operations2, data2);
// 预期输出: "FFFFFFFFFFFFFFFFFFFFFF5151515151ADADADADADADADADFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
}
// 辅助函数,用于运行测试用例
private static void runTest(int bufferCap, int[][] operations, String initialData) {
// LeetCode 风格的调用方式,它会处理好输入并调用这些方法
WriteBufferSystem system = new WriteBufferSystem(bufferCap, initialData);
for (int[] op : operations) {
switch (op[0]) {
case 1:
system.write(op[1], op[2], op[3]);
break;
case 2:
system.read(op[1], op[2]);
break;
case 3:
system.sync();
break;
}
}
System.out.println(system.getFinalMemoryState());
}
}