最近在准备华为OD机考的同学们应该都注意到了C卷中出现的这道经典题目——单核CPU任务调度。这道题看似简单,实则暗藏玄机,非常考验对Java集合操作和时间复杂度的把控能力。我在实际机考和日常开发中多次遇到类似场景,今天就来拆解这道题的解题思路和优化技巧。
题目通常给出一个任务队列,每个任务包含到达时间和执行时长两个参数。我们需要模拟单核CPU按照特定规则处理这些任务的过程,并计算平均等待时间。关键在于理解任务调度的优先级规则:当CPU空闲时优先处理最先到达的任务;如果多个任务同时到达,则选择执行时间最短的任务(SJF短作业优先)。
首先我们需要定义合适的数据结构来表示任务。这里我推荐使用记录类(Record)来封装任务属性:
java复制record Task(int arrival, int duration) implements Comparable<Task> {
@Override
public int compareTo(Task o) {
return Integer.compare(this.arrival, o.arrival);
}
}
输入处理阶段要注意边界条件检查。题目通常给出形如[[1,4],[2,3],[5,2]]的二维数组,我们需要将其转换为Task对象列表:
java复制List<Task> tasks = Arrays.stream(input)
.map(arr -> new Task(arr[0], arr[1]))
.collect(Collectors.toList());
调度算法的核心在于维护两个关键数据结构:
java复制PriorityQueue<Task> readyQueue = new PriorityQueue<>(
(a,b) -> a.duration != b.duration
? a.duration - b.duration
: a.arrival - b.arrival
);
int currentTime = 0;
int totalWait = 0;
int index = 0;
算法主循环需要处理三种情况:
java复制while (index < tasks.size() || !readyQueue.isEmpty()) {
// 将当前时间点前到达的任务加入就绪队列
while (index < tasks.size() && tasks.get(index).arrival <= currentTime) {
readyQueue.offer(tasks.get(index++));
}
if (!readyQueue.isEmpty()) {
Task current = readyQueue.poll();
totalWait += currentTime - current.arrival;
currentTime += current.duration;
} else {
// CPU空闲时直接跳到下一个任务到达时间
currentTime = tasks.get(index).arrival;
}
}
很多同学会疑惑为什么使用两个不同的优先队列:
这种设计使得:
总体时间复杂度控制在O(nlogn),完全满足题目要求。
实际编码时需要特别注意几个边界case:
这里分享一个验证边界条件的测试用例集:
java复制Object[][] testCases = {
// 常规情况
{new int[][]{{1,4},{2,3},{5,2}}, 3.0},
// 同时到达任务
{new int[][]{{1,4},{1,3},{1,2}}, 3.0},
// 无序到达
{new int[][]{{5,2},{1,4},{3,1}}, 3.666},
// 单任务
{new int[][]{{0,10}}, 0.0}
};
结合上述分析,完整的解决方案如下:
java复制import java.util.*;
public class CPUScheduler {
record Task(int arrival, int duration) implements Comparable<Task> {
@Override
public int compareTo(Task o) {
return Integer.compare(this.arrival, o.arrival);
}
}
public static double scheduleTasks(int[][] input) {
if (input == null || input.length == 0) return 0;
List<Task> tasks = Arrays.stream(input)
.map(arr -> new Task(arr[0], arr[1]))
.sorted()
.collect(Collectors.toList());
PriorityQueue<Task> readyQueue = new PriorityQueue<>(
(a,b) -> a.duration != b.duration
? a.duration - b.duration
: a.arrival - b.arrival
);
int currentTime = 0;
int totalWait = 0;
int index = 0;
while (index < tasks.size() || !readyQueue.isEmpty()) {
while (index < tasks.size() && tasks.get(index).arrival <= currentTime) {
readyQueue.offer(tasks.get(index++));
}
if (!readyQueue.isEmpty()) {
Task current = readyQueue.poll();
totalWait += currentTime - current.arrival;
currentTime += current.duration;
} else if (index < tasks.size()) {
currentTime = tasks.get(index).arrival;
}
}
return (double) totalWait / input.length;
}
}
在机考环境下调试这类算法题时,建议:
例如可以添加如下调试代码:
java复制System.out.println("Processing task: " + current +
" at time: " + currentTime +
" wait time: " + (currentTime - current.arrival));
对于大规模任务输入(虽然机考通常不会),我们可以优化内存使用:
优化后的数据结构:
java复制Task[] tasks = Arrays.stream(input)
.map(arr -> new Task(arr[0], arr[1]))
.sorted()
.toArray(Task[]::new);
这道题目有多个变种形式值得思考:
以多核CPU为例,算法需要改为维护多个执行时间线,并合理分配任务到不同核心。核心修改点:
java复制// 使用数组记录各CPU状态
int[] cpuEndTimes = new int[k]; // k为CPU核心数
// 分配任务时选择最早空闲的CPU
int cpu = findEarliestCPU(cpuEndTimes);
totalWait += Math.max(cpuEndTimes[cpu] - task.arrival, 0);
cpuEndTimes[cpu] = Math.max(cpuEndTimes[cpu], task.arrival) + task.duration;
参加过多次OD机考后,我总结出这类题目的一些通关技巧:
特别注意:机考环境可能没有IDE自动补全,要熟悉常用API的手写方式,比如PriorityQueue的构造函数写法。