1. 问题背景与需求分析
在大型应用系统启动过程中,初始化任务的执行顺序往往存在复杂的依赖关系。以华为OD机考双机位C卷中的这道题目为例,我们需要设计一个能够正确处理任务依赖关系的排序算法。这类问题在实际开发中非常常见,比如Spring框架的Bean初始化、前端构建工具的依赖解析等场景。
1.1 问题核心要点
题目要求我们实现以下功能:
- 解析输入的任务依赖关系(如"A->B"表示A依赖B)
- 根据依赖关系生成合法的任务执行顺序
- 当多个任务可同时执行时,按任务名称字母顺序排序
- 处理可能存在的循环依赖等异常情况
注意:题目明确要求采用"贪婪策略",即一旦任务满足执行条件就立即执行,这与传统的拓扑排序有所不同。
2. 算法设计与选型
2.1 拓扑排序的变种
这个问题本质上是拓扑排序的变种,但有以下特殊要求:
- 需要处理并行执行的情况
- 当多个任务可并行执行时,按字母顺序选择
- 需要检测可能的循环依赖
java复制// 基础数据结构示例
Map<String, List<String>> dependencyGraph = new HashMap<>(); // 任务依赖关系图
Map<String, Integer> inDegree = new HashMap<>(); // 每个任务的入度计数
2.2 具体实现思路
- 构建依赖图:使用邻接表表示任务依赖关系
- 计算入度:统计每个任务的前置依赖数量
- 优先队列管理:使用最小堆(按字母顺序)管理可执行任务
- 执行过程:
- 将入度为0的任务加入队列
- 每次取出队首任务执行
- 更新依赖该任务的其他任务的入度
- 将新产生的入度为0的任务加入队列
3. Java实现详解
3.1 完整代码实现
java复制import java.util.*;
public class TaskScheduler {
public List<String> getTaskOrder(String[] dependencies) {
// 初始化数据结构
Map<String, List<String>> graph = new HashMap<>();
Map<String, Integer> inDegree = new HashMap<>();
Set<String> allTasks = new HashSet<>();
// 1. 解析输入并构建依赖图
for (String dep : dependencies) {
String[] parts = dep.split("->");
String task = parts[0].trim();
String preReq = parts[1].trim();
graph.putIfAbsent(preReq, new ArrayList<>());
graph.get(preReq).add(task);
inDegree.put(task, inDegree.getOrDefault(task, 0) + 1);
allTasks.add(task);
allTasks.add(preReq);
}
// 2. 初始化优先队列(最小堆)
PriorityQueue<String> queue = new PriorityQueue<>();
for (String task : allTasks) {
if (inDegree.getOrDefault(task, 0) == 0) {
queue.offer(task);
}
}
// 3. 执行拓扑排序
List<String> result = new ArrayList<>();
while (!queue.isEmpty()) {
String current = queue.poll();
result.add(current);
if (graph.containsKey(current)) {
for (String neighbor : graph.get(current)) {
inDegree.put(neighbor, inDegree.get(neighbor) - 1);
if (inDegree.get(neighbor) == 0) {
queue.offer(neighbor);
}
}
}
}
// 4. 检查循环依赖
if (result.size() != allTasks.size()) {
throw new RuntimeException("存在循环依赖,无法完成所有任务");
}
return result;
}
public static void main(String[] args) {
TaskScheduler scheduler = new TaskScheduler();
String[] dependencies = {"B->A", "C->A", "D->B", "D->C", "D->E"};
List<String> order = scheduler.getTaskOrder(dependencies);
System.out.println("任务执行顺序: " + String.join(", ", order));
}
}
3.2 关键代码解析
-
数据结构选择:
graph:使用HashMap存储每个任务的后继任务列表inDegree:记录每个任务的当前未完成的前置任务数量PriorityQueue:确保每次取出字母顺序最小的可执行任务
-
循环依赖检测:
- 最终结果列表长度应等于总任务数
- 如果不等,说明存在无法解决的循环依赖
-
时间复杂度分析:
- 构建图:O(E),E为依赖关系数量
- 拓扑排序:O(VlogV + E),V为任务数量
4. 测试用例与验证
4.1 基础测试用例
java复制@Test
public void testBasicCase() {
String[] deps = {"B->A", "C->A", "D->B", "D->C", "D->E"};
List<String> result = new TaskScheduler().getTaskOrder(deps);
assertEquals(Arrays.asList("A", "E", "B", "C", "D"), result);
}
4.2 边界情况测试
-
无依赖任务:
java复制@Test public void testNoDependencies() { String[] deps = {"A", "B", "C"}; List<String> result = new TaskScheduler().getTaskOrder(deps); assertEquals(Arrays.asList("A", "B", "C"), result); } -
循环依赖检测:
java复制@Test(expected = RuntimeException.class) public void testCycleDetection() { String[] deps = {"A->B", "B->C", "C->A"}; new TaskScheduler().getTaskOrder(deps); } -
并行任务排序:
java复制@Test public void testParallelTaskOrder() { String[] deps = {"C->A", "B->A", "D->B", "D->C", "F->E"}; List<String> result = new TaskScheduler().getTaskOrder(deps); // A和E应该先执行,且A在E前(字母顺序) assertEquals(Arrays.asList("A", "E", "B", "C", "D", "F"), result); }
5. 性能优化与工程实践
5.1 大规模数据处理优化
当任务数量很大时(如超过10万),可以考虑以下优化:
- 使用更高效的数据结构,如Trie树存储任务名
- 并行处理独立的任务分支
- 分批处理依赖关系,减少内存占用
5.2 工程实践建议
-
日志记录:在执行过程中添加详细日志,便于调试
java复制System.out.println("Executing task: " + current); -
进度反馈:对于长时间运行的任务,提供进度回调
java复制interface ProgressCallback { void onProgress(int completed, int total); } -
异常处理:细化异常类型,区分不同错误场景
java复制class CycleDependencyException extends RuntimeException { // 自定义异常处理 }
6. 常见问题与解决方案
6.1 循环依赖处理
问题现象:程序抛出"存在循环依赖"异常
解决方案:
- 在构建依赖图时检测强连通分量
- 使用Tarjan算法或Kosaraju算法识别循环
- 提供更详细的循环路径信息
java复制// 增强的循环检测
private void checkCycles(Map<String, List<String>> graph) {
// 实现强连通分量检测算法
}
6.2 性能瓶颈
问题现象:处理大规模数据时速度变慢
优化方案:
- 使用并发数据结构
- 并行执行独立任务
- 采用增量处理方式
6.3 字母顺序的特殊情况
问题现象:任务名称包含数字或特殊字符时排序不符合预期
解决方案:
- 自定义Comparator处理复杂名称
java复制Comparator<String> taskComparator = (a, b) -> { // 自定义比较逻辑 };
7. 算法扩展与变种
7.1 带权重的任务调度
如果需要考虑任务执行时间,可以扩展为:
java复制class WeightedTask {
String name;
int duration;
// getters/setters
}
// 在优先队列中考虑任务权重
PriorityQueue<WeightedTask> queue = new PriorityQueue<>(
Comparator.comparingInt(WeightedTask::getDuration)
);
7.2 多线程任务执行
实际工程中可能需要真正并行执行任务:
java复制ExecutorService executor = Executors.newFixedThreadPool(4);
while (!queue.isEmpty()) {
String task = queue.poll();
executor.submit(() -> executeTask(task));
}
7.3 可视化依赖关系
使用Graphviz等工具生成依赖图:
java复制// 生成DOT语言描述的图
StringBuilder dot = new StringBuilder("digraph G {");
for (Map.Entry<String, List<String>> entry : graph.entrySet()) {
for (String dep : entry.getValue()) {
dot.append("\n \"" + entry.getKey() + "\" -> \"" + dep + "\"");
}
}
dot.append("\n}");
在实际开发中,这类任务调度问题会变得更加复杂。我在某次系统重构时就遇到过类似场景,当时由于没有正确处理循环依赖,导致系统启动时出现死锁。后来通过引入类似本文的拓扑排序算法,并结合详细的日志记录,最终解决了问题。关键是要记住:任何时候修改依赖关系,都要重新运行完整的测试用例。