问题解析
小M需要计算在一组任务中,最多有多少个任务在同一时刻同时下载。每个任务都有一个开始时间和持续时间。为了找到最高的并发数,我们可以使用事件计数的方法。
思路
1. 事件记录:
- 对于每个任务,我们可以记录两个事件:任务的开始和结束。
- 任务的开始时间为 x,结束时间为 x + y。
2. 事件排序:
- 将所有事件按时间排序。如果两个事件的时间相同,结束事件优先于开始事件。这是因为如果一个任务在另一个任务开始时结束,我们希望在计算并发数时不将其计入。
3.并发数计算:
- 遍历所有事件,维护一个当前并发数的计数器。
- 每当遇到一个开始事件,增加计数器;每当遇到一个结束事件,减少计数器。
- 在遍历过程中,记录最大并发数。
图解
假设有以下任务:
- 任务1:开始时间 1,持续时间 3(结束时间 4)
- 任务2:开始时间 2,持续时间 2(结束时间 4)
- 任务3:开始时间 4,持续时间 1(结束时间 5)
事件记录如下:
- (1, 1) // 任务1开始
- (4, -1) // 任务1结束
- (2, 1) // 任务2开始
- (4, -1) // 任务2结束
- (4, 1) // 任务3开始
- (5, -1) // 任务3结束
排序后的事件:
- (1, 1)
- (2, 1)
- (4, -1)
- (4, -1)
- (4, 1)
- (5,-1)
代码详解
1.事件数组
int[][] events = new int[n * 2][2];
- 创建一个二维数组 events,大小为 n * 2,用于存储每个任务的开始和结束事件。每个事件包含两个元素:时间和事件类型(开始或结束)。
2.时件记录
// 记录每个任务的开始和结束时间
for (int i = 0; i < n; i++) {
events[i * 2][0] = array[i][0]; // 开始时间
events[i * 2][1] = 1; // 任务开始
events[i * 2 + 1][0] = array[i][0] + array[i][1]; // 结束时间
events[i * 2 + 1][1] = -1; // 任务结束
}
- 使用一个循环遍历每个任务。
- events[i * 2][0] = array[i][0]:将任务的开始时间存入事件数组。
- events[i * 2][1] = 1:标记该事件为开始事件。
- events[i * 2 + 1][0] = array[i][0] + array[i][1]:计算并存储任务的结束时间。
- events[i * 2 + 1][1] = -1:标记该事件为结束事件。
3.事件按时间排序
// 按时间排序
Arrays.sort(events, (a, b) -> {
if (a[0] != b[0]) {
return Integer.compare(a[0], b[0]);
}
return Integer.compare(a[1], b[1]); // 结束事件优先
});
- 使用 Arrays.sort 方法对事件数组进行排序。
- 排序规则: 1.首先按时间(a[0] 和 b[0])进行升序排序。 2.如果时间相同,结束事件(b[1] 为 -1)优先于开始事件(a[1] 为 1),确保在同一时刻结束的任务不会被计入并发数。
4.并发数计算并返回
int maxConcurrent = 0;
int currentConcurrent = 0;
// 遍历事件,计算并发任务数
for (int[] event : events) {
currentConcurrent += event[1];
maxConcurrent = Math.max(maxConcurrent, currentConcurrent);
}
return maxConcurrent;
- maxConcurrent:用于记录最大并发数。
- currentConcurrent:用于记录当前的并发数。
- 遍历排序后的事件数组。
- currentConcurrent += event[1]:根据事件类型更新当前并发数。如果是开始事件,增加 1;如果是结束事件,减少 1。
- maxConcurrent = Math.max(maxConcurrent, currentConcurrent):更新最大并发数,确保记录到当前的最大值
个人思考与分析
- 时间复杂度:排序的时间复杂度为 O(n log n),遍历事件的时间复杂度为 O(n),因此整体时间复杂度为 O(n log n)。
- 空间复杂度:需要额外的 O(n) 空间来存储事件。
- 边界情况:需要考虑任务数量为 0 的情况,此时并发数应为 0。
通过这种方法,我们可以有效地计算出在任何时刻的最大并发任务数,满足小M的需求。对这一并发计算问题进行深入思考,我们不仅能够实现一个高效的算法,还能为未来的扩展和优化打下基础。理解问题的复杂性、选择合适的算法、处理边界情况以及考虑实际应用场景,都是构建高质量软件的重要组成部分。