模拟磁盘 I/O 的优化过程

83 阅读8分钟

模拟磁盘 I/O 的优化过程

背景介绍

在计算机系统中,磁盘被划分为连续的扇区 (Sector) 。为了提升性能,系统会尝试将多个分散的读写操作合并成一个大的连续操作,以减少磁头的寻道次数。在执行这个大的连续操作时,又需要将其分解为与底层扇区对齐的多个小操作。

你的任务就是模拟这个“先合并,后拆分”的磁盘 I/O 优化过程。

核心概念

  1. 地址区间 (Address Interval):

    • 一个读操作由一个闭区间 [startAddr, endAddr] 定义,表示需要读取从 startAddrendAddr 的所有字节。
  2. 扇区 (Sector):

    • 磁盘地址空间被划分为大小为 sectorSize 的连续扇区。
    • n 个扇区(从 0 开始计数)覆盖的地址范围是:[sectorSize * n, sectorSize * (n + 1) - 1]

任务要求

你需要实现一个程序,接收扇区大小 sectorSize 和一系列读操作 opArray,并返回一组经过合并拆分后的、最优的读操作区间列表。整个过程分两个阶段:

第一阶段:合并地址区间 (Merge Intervals)
  • 将所有输入的、可能重叠或连续的读操作区间 opArray 合并成一组互不重叠的、尽可能大的连续区间。

  • 合并规则:

    • 如果两个区间有重叠,例如 [0, 30][20, 50],它们会被合并成 [0, 50]
    • 即使是相邻的区间(如 [130, 150][151, 158]),也应被视为一个大的连续操作,合并为 [130, 158]。(注:此处的合并规则需要根据具体实现调整,但通常指有交集或紧邻可合并)。更新:根据样例,[150][151] 紧邻,应合并。
第二阶段:按扇区拆分 (Split by Sectors)
  • 将第一阶段合并好的每一个“大区间”,按照扇区的边界进行拆分。

  • 拆分规则: 如果一个合并后的区间跨越了一个或多个扇区的边界,那么它必须在这些边界处被切分开。

  • 示例:

    • 假设 sectorSize = 32,一个合并后的大区间为 [60, 100]
    • 这个区间跨越了两个扇区边界:扇区 1 的末尾 (63) 和扇区 2 的末尾 (95)。
    • 因此,[60, 100] 会被拆分为三个新的、与扇区对齐的区间:[60, 63], [64, 95], [96, 100]

最终输出要求

  • 返回一个二维数组或列表,包含所有最终生成的、经过合并与拆分的地址区间。
  • 结果列表必须按照区间的起始地址从小到大排序。

输入格式

  1. 第一个参数 sectorSize:

    • 扇区大小。
    • 32 <= sectorSize <= 2048,且 sectorSize 保证为 2 的幂。
  2. 第二个参数 opArray:

    • 一个二维数组,包含一系列读操作 [startAddr, endAddr]
    • 0 <= opArray.length <= 10000
    • 0 <= opArray[i].startAddr <= opArray[i].endAddr < 2^31 - 1

输出格式

  • 一个列表或二维数组,表示排序后的最终地址区间,每个元素的格式为 [startTime, endTime]
  • 如果没有操作,则输出空列表 []

样例

输入样例 1

32
[[0, 30], [10, 33], [130, 150], [151, 158], [60, 100], [130, 150], [20, 50]]

输出样例 1

[[0, 31], [32, 50], [60, 63], [64, 95], [96, 100], [130, 158]]

样例 1 执行流程详解

  1. 阶段一:合并地址区间

    • 原始输入: [[0, 30], [10, 33], [130, 150], [151, 158], [60, 100], [130, 150], [20, 50]]

    • 排序后 (按 startAddr): [[0, 30], [10, 33], [20, 50], [60, 100], [130, 150], [130, 150], [151, 158]]

    • 合并过程:

      • [0, 30]
      • [10, 33] 合并 -> [0, 33]
      • [20, 50] 合并 -> [0, 50]
      • [60, 100] 不与 [0, 50] 重叠,成为新的合并区间。
      • [130, 150] 不与 [60, 100] 重叠,成为新的合并区间。
      • 与下一个 [130, 150] 合并 -> [130, 150] (不变)
      • [151, 158] 连续,合并 -> [130, 158]
    • 合并结果: [[0, 50], [60, 100], [130, 158]]

  2. 阶段二:按扇区拆分 (sectorSize = 32)

    • 处理 [0, 50]:

      • 跨越了扇区 0 的边界 31
      • 拆分为 [0, 31][32, 50]
    • 处理 [60, 100]:

      • 跨越了扇区 1 的边界 63 和扇区 2 的边界 95
      • 拆分为 [60, 63], [64, 95], [96, 100]
    • 处理 [130, 158]:

      • 跨越了扇区 4 的边界 159?没有。130158 都在扇区 4 [128, 159] 内。
      • 不跨越边界,保持为 [130, 158]
  3. 最终输出: 汇集所有拆分后的区间,并按起始地址排序(它们自然已经有序)。

[[0, 31], [32, 50], [60, 63], [64, 95], [96, 100], [130, 158]]

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedList; // LinkedList is efficient for accessing/modifying the last element
import java.util.List;

/**
 * 算法思想:
 * 这是一个两阶段的过程:先合并,再拆分。
 *
 * 1.  **阶段一:合并区间 (Merge Intervals)**
 * -   这是一个经典的区间合并问题。为了有效地合并所有重叠或连续的区间,
 * 首先必须对所有区间按**起始地址**进行升序排序。
 * -   排序后,我们遍历区间列表,维护一个已合并的区间列表。对于每个新区间,
 * 我们检查它是否与已合并的最后一个区间有重叠或连续。
 * -   如果有,就将它们合并(即更新最后一个合并区间的结束地址)。
 * -   如果没有,就将这个新区间作为一个独立的合并区间添加到列表中。
 *
 * 2.  **阶段二:按扇区拆分 (Split by Sectors)**
 * -   在第一阶段得到一组不重叠的、合并后的大区间后,我们需要遍历这组大区间中的每一个。
 * -   对于每一个大区间 `[start, end]`,我们检查它是否跨越了扇区的边界。
 * -   一个扇区的范围是 `[k * sectorSize, (k+1) * sectorSize - 1]`。
 * -   我们从区间的 `start` 开始,计算出它所在扇区的结束地址 `sectorEnd`。
 * -   当前需要生成的子区间的结束地址就是 `min(end, sectorEnd)`。
 * -   生成这个子区间后,更新下一次处理的起始地址为 `当前子区间结束地址 + 1`,
 * 然后重复这个过程,直到处理完整个大区间 `[start, end]`。
 *
 * 最终,将所有拆分后的子区间收集起来,就是问题的答案。
 */
public class Solution {

    /**
     * 对所有读操作的地址区间进行合并,然后把合并后的地址区间按扇区进行拆分。
     *
     * @param sectorSize 扇区大小
     * @param opArray    一系列读操作,每个操作为 [startAddr, endAddr]
     * @return 按照地址从小到大排序的、最终的地址区间列表
     */
    public int[][] processIORequests(int sectorSize, int[][] opArray) {
        // --- 1. 边界情况处理 ---
        // 如果没有操作,直接返回空的结果数组
        if (opArray == null || opArray.length == 0) {
            return new int[0][];
        }

        // --- 2. 阶段一:合并区间 ---

        // a. 按区间的起始地址(startAddr)对所有操作进行升序排序
        // 这是合并区间的关键前提步骤。
        // Comparator.comparingInt(a -> a[0]) 是一个简洁的写法,表示按内部数组的第一个元素排序。
        Arrays.sort(opArray, Comparator.comparingInt(a -> a[0]));

        // b. 遍历并合并重叠或连续的区间
        // 使用 LinkedList 方便地访问和修改最后一个元素 (getLast(), set())
        LinkedList<int[]> mergedIntervals = new LinkedList<>();
        mergedIntervals.add(opArray[0]); // 首先将第一个区间加入合并列表

        for (int i = 1; i < opArray.length; i++) {
            int[] currentInterval = opArray[i];
            int[] lastMerged = mergedIntervals.getLast();

            // 检查当前区间是否与最后一个合并区间重叠或紧邻
            // 如果 current_start <= last_end + 1,则它们需要合并
            // 例如 [10, 20] 和 [21, 30] 是紧邻的,也需要合并
            if (currentInterval[0] <= lastMerged[1] + 1) {
                // 如果是,则合并它们:更新最后一个合并区间的结束地址为两者结束地址中的较大者
                lastMerged[1] = Math.max(lastMerged[1], currentInterval[1]);
            } else {
                // 如果不重叠,则将当前区间作为一个新的、独立的合并区间添加到列表中
                mergedIntervals.add(currentInterval);
            }
        }

        // --- 3. 阶段二:按扇区拆分 ---
        List<int[]> finalIntervals = new ArrayList<>();

        // 遍历所有合并后的大区间
        for (int[] interval : mergedIntervals) {
            // 使用 long 类型进行计算,以防止地址和 sectorSize 相乘时溢出
            long start = interval[0];
            long end = interval[1];
            long currentStart = start;

            // 只要当前处理的起始地址没有超过区间的结束地址,就继续拆分
            while (currentStart <= end) {
                // 计算 currentStart 所在扇区的结束地址
                // sectorEnd = (currentStart / sectorSize + 1) * sectorSize - 1
                long sectorEnd = (currentStart / sectorSize + 1) * (long)sectorSize - 1;

                // 当前拆分出的子区间的结束地址,是扇区结束地址和整个大区间结束地址中的较小者
                long pieceEnd = Math.min(end, sectorEnd);

                // 将拆分出的子区间 [currentStart, pieceEnd] 加入最终结果列表
                finalIntervals.add(new int[]{(int) currentStart, (int) pieceEnd});

                // 更新下一个子区间的起始地址
                currentStart = pieceEnd + 1;
            }
        }

        // --- 4. 格式化返回结果 ---
        // 将 List<int[]> 转换为 int[][]
        return finalIntervals.toArray(new int[0][]);
    }
}


/**
 * 用于处理 ACM 风格输入输出的主类。
 */
class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        // 读取扇区大小
        int sectorSize = Integer.parseInt(scanner.nextLine());
        
        // 读取操作列表的字符串表示
        String opArrayLine = scanner.nextLine();
        
        scanner.close();
        
        // --- 解析输入 ---
        // 解析形如 [[0, 30], [10, 33], ...] 的字符串
        // 移除首尾的 "[[" 和 "]]"
        opArrayLine = opArrayLine.replaceAll("^\\[{2}|]$", "").trim();
        opArrayLine = opArrayLine.substring(0, opArrayLine.length() - 1);
        
        int[][] opArray;
        if (opArrayLine.isEmpty()) {
            opArray = new int[0][0];
        } else {
            String[] pairs = opArrayLine.split("\\],\\s*\\[");
            opArray = new int[pairs.length][2];
            for (int i = 0; i < pairs.length; i++) {
                String[] nums = pairs[i].split(",");
                opArray[i][0] = Integer.parseInt(nums[0].trim());
                opArray[i][1] = Integer.parseInt(nums[1].trim());
            }
        }
        
        // 创建 Solution 类的实例并调用方法
        Solution solution = new Solution();
        int[][] result = solution.processIORequests(sectorSize, opArray);
        
        // 按题目要求的格式打印输出
        System.out.println(Arrays.deepToString(result).replace(" ", ""));
    }
}