带 WriteBuffer 的内存读写操作

题目:带 WriteBuffer 的内存读写操作

背景介绍

想象一下,你是一位非常繁忙的大公司 CEO(中央处理器 CPU),你的工作是飞速地处理各种事务。而公司的中央档案库(主存 data)虽然巨大且可靠,但管理员(内存控制器)的动作很慢。

如果你每做一个小决定(比如修改一份文件的某一个字节),都要亲自跑到档案库,等待管理员找到文件、修改、再归档,那你宝贵的时间就都浪费在等待上了。这显然无法忍受。

为了解决这个问题,你聘请了一位行动非常迅速的秘书,并给了他/她一个很小的待办事项记事本。这个记事本就是 WriteBuffer (写缓存)


现在,我们来认识一下这个系统的三个核心部分:

  1. 你 (CPU): 负责发出指令,是整个系统的决策者。

  2. 中央档案库 (data): 这是最终存储所有信息的地方,巨大、永久但访问速度慢。它在我们的问题中由一长串十六进制字符串表示。

  3. 你的秘书和记事本 (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 无意义)
  • 意图: 将缓存中的所有写请求都取出来并写入主存,清空缓存。

输入格式

  1. 第一个参数 bufferCap:

    • 表示 WriteBuffer 的容量(最多能缓存的请求个数)。
    • 4 <= bufferCap <= 32
  2. 第二个参数 operations:

    • 一个二维数组,包含一系列操作指令。
    • 1 <= operations.length <= 1024
    • 1 <= op <= 3
    • para1data 地址范围之内。
    • 0 <= para2 <= bufferCap * 8 (当 op 为读或写时,para2 >= 1)。
    • 0 <= para3 <= 255
  3. 第三个参数 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, 1, 3, 255]: 写操作。分解为 1 个写请求 [offset=1, len=3, val=0xFF]。缓存未满,直接入队。

    • 缓存状态: {[1,3,255]}
    • 主存: (不变) 1DA082...
  2. [3, 0, 0, 0]: 同步操作。

    • 将缓存中所有请求写入主存。
    • 缓存状态: [] (空)
    • 主存: 1DFFFFFF0000...
  3. [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...
  4. [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());
    }
}
相关推荐
矢志航天的阿洪3 分钟前
蒙特卡洛树搜索方法实践
算法
UnderTheTime36 分钟前
2025 XYD Summer Camp 7.10 筛法
算法
zstar-_36 分钟前
Claude code在Windows上的配置流程
笔记·算法·leetcode
圆头猫爹1 小时前
第34次CCF-CSP认证第4题,货物调度
c++·算法·动态规划
27669582921 小时前
tiktok 弹幕 逆向分析
java·python·tiktok·tiktok弹幕·tiktok弹幕逆向分析·a-bogus·x-gnarly
秋说1 小时前
【PTA数据结构 | C语言版】出栈序列的合法性
c语言·数据结构·算法
用户40315986396631 小时前
多窗口事件分发系统
java·算法
用户40315986396631 小时前
ARP 缓存与报文转发模拟
java·算法
小林ixn1 小时前
大一新手小白跟黑马学习的第一个图形化项目:拼图小游戏(java)
java
nbsaas-boot1 小时前
Go语言生态成熟度分析:为何Go还无法像Java那样实现注解式框架?
java·开发语言·golang