模拟磁盘 I/O 的优化过程
背景介绍
在计算机系统中,磁盘被划分为连续的扇区 (Sector) 。为了提升性能,系统会尝试将多个分散的读写操作合并成一个大的连续操作,以减少磁头的寻道次数。在执行这个大的连续操作时,又需要将其分解为与底层扇区对齐的多个小操作。
你的任务就是模拟这个“先合并,后拆分”的磁盘 I/O 优化过程。
核心概念
-
地址区间 (Address Interval):
- 一个读操作由一个闭区间
[startAddr, endAddr]定义,表示需要读取从startAddr到endAddr的所有字节。
- 一个读操作由一个闭区间
-
扇区 (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]。
- 假设
最终输出要求
- 返回一个二维数组或列表,包含所有最终生成的、经过合并与拆分的地址区间。
- 结果列表必须按照区间的起始地址从小到大排序。
输入格式
-
第一个参数
sectorSize:- 扇区大小。
32 <= sectorSize <= 2048,且sectorSize保证为 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 执行流程详解
-
阶段一:合并地址区间
-
原始输入:
[[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]]
-
-
阶段二:按扇区拆分 (sectorSize = 32)
-
处理
[0, 50]:- 跨越了扇区 0 的边界
31。 - 拆分为
[0, 31]和[32, 50]。
- 跨越了扇区 0 的边界
-
处理
[60, 100]:- 跨越了扇区 1 的边界
63和扇区 2 的边界95。 - 拆分为
[60, 63],[64, 95],[96, 100]。
- 跨越了扇区 1 的边界
-
处理
[130, 158]:- 跨越了扇区 4 的边界
159?没有。130和158都在扇区 4[128, 159]内。 - 不跨越边界,保持为
[130, 158]。
- 跨越了扇区 4 的边界
-
-
最终输出: 汇集所有拆分后的区间,并按起始地址排序(它们自然已经有序)。
[[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(" ", ""));
}
}