
华为OD机试 新系统 题库疯狂收录中,刷题点这里
专栏导读
本专栏收录于《华为OD机试(JAVA)真题》。
刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天CSDN在线答疑。
一、题目描述
在软件项目开发里涉及不同版本组件引用,组件依赖组件,形成一棵依赖树(从根节点深层展开);请编写一段代码,对当前项目的组件依赖关系进行分析,按项目组件依赖顺序输出最终有效的组件版本列表;
说明如下:
- 从根节点开始按依赖顺序输出组件列表,项目上层组件依赖先输出,如果同层依赖则父组件先出现的优先输出,父组件相同的按依赖出现顺序输出,如:项目依赖组件【b,a】,a依赖【c,d】,b依赖【e】,输出顺序为 b,a,e,c,d;
- 组件去重:依赖组件如多次出现,只保留第一次出现的顺序位置;
- 同一组件如有不同版本,以首次依赖的组件版本作为输出;
二、输入描述
-
参数1:项目直属依赖
directDeps,格式:组件名:版本号:排除组件列表,如:a:v1:表示项目依赖a组件,版本为v1,无排除组件; -
参数2:全局组件依赖
depRules,格式:组件名:依赖组件名:依赖组件版本号:排除组件列表",如:a:b:v2:c,表示a组件依赖b组件v2版本,须排除b组件依赖的c组件子树;
输入列表顺序为依赖声明顺序;组件名、版本号由字母数字组成,非空;输入保证同一组件不同版本依赖相同;排除组件列表以半角逗号分隔,格式如:“a,b”,排除列表可为空,输入保证排除列表不会出现",b"或"a,"格式;
三、输出描述
符合依赖顺序要求的项目依赖组件列表,格式:组件名:版本号
四、测试用例
测试用例1:
1、输入
a:v1:
2、输出
a:v1
3、说明
没有任何间接依赖,所以直接输出 a:v1。
测试用例2:
1、输入
a:v1: b:v1:
a:c:v1: b:d:v1: d:c:v2:
2、输出
a:v1 b:v1 c:v1 d:v1
3、说明
第1层:
a:v1 b:v1
第2层:
a -> c:v1
b -> d:v1
得到:a b c d
之后:d -> c:v2
但 c 已经以 v1 第一次出现,所以忽略 c:v2。
五、解题思路
本题的核心是按照依赖树的层级顺序输出组件,因此采用 BFS(广度优先搜索) 遍历依赖关系,而不是 DFS。先把项目的直属依赖按输入顺序加入队列,再逐层处理每个组件的子依赖,这样可以保证“上层组件优先、同层按父组件出现顺序、同一父组件按依赖声明顺序”这一输出要求。
主要采用了以下数据结构:
邻接表 Map<String, List>:保存每个组件对应的依赖组件列表。List 可以保留依赖声明的原始顺序,适合表示组件依赖关系。
队列 Queue:用于 BFS 层序遍历,保证先处理上层组件,再处理下一层组件。
LinkedHashMap:保存组件第一次出现时的版本及顺序。它既可以实现组件去重,又能保留第一次插入顺序,因此可以满足“同一组件只保留第一次出现的位置和版本”的要求。
HashSet:保存当前依赖路径上生效的排除组件,查询某组件是否被排除的效率较高。
算法处理时,每遇到一个新的组件,先判断它是否被当前依赖路径排除;如果没有被排除,再判断是否已经出现过。只有第一次出现的组件才加入结果和 BFS 队列,并将当前依赖边上的排除项合并后继续向其子树传递。
整体上可以理解为:
构建依赖邻接表
↓
直属依赖按顺序加入 BFS 队列
↓
逐层遍历依赖关系
↓
排除当前路径不允许的组件
↓
LinkedHashMap 去重并保留首次版本
↓
按首次出现顺序输出
在不考虑排除集合复制开销的情况下,主体遍历的时间复杂度约为 O(V + E),其中 V 是组件数量,E 是依赖关系数量;空间复杂度约为 O(V + E)。
六、Java算法源码
public class OdTest {
/**
* 表示一条依赖关系。
*
* 例如:
* a:b:v2:c,d
*
* 表示 a 依赖 b:v2,
* 并且进入 b 的依赖子树以后,需要排除 c、d。
*/
static class Edge {
String childName;
String version;
Set<String> exclusions;
Edge(String childName, String version, Set<String> exclusions) {
this.childName = childName;
this.version = version;
this.exclusions = exclusions;
}
}
/**
* BFS 队列中的节点。
*
* activeExclusions 表示:
* 从项目根节点到当前组件这条依赖路径上,
* 当前仍然生效的所有排除项。
*/
static class Node {
String name;
String version;
Set<String> activeExclusions;
Node(String name, String version, Set<String> activeExclusions) {
this.name = name;
this.version = version;
this.activeExclusions = activeExclusions;
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 第一行:项目直属依赖 directDeps
String directLine = scanner.hasNextLine()
? scanner.nextLine().trim()
: "";
// 第二行:全局组件依赖 depRules
// 没有依赖规则时第二行可以不存在
String ruleLine = scanner.hasNextLine()
? scanner.nextLine().trim()
: "";
/*
* 保存全局依赖规则。
*
* List 必须严格保留输入顺序。
* 例如:
* a:c:v1: a:d:v1:
*
* 必须保证处理 a 时先处理 c,再处理 d。
*/
Map<String, List<Edge>> rules = new HashMap<>();
if (!ruleLine.isEmpty()) {
String[] ruleItems = ruleLine.split("\\s+");
for (String item : ruleItems) {
/*
* split(":", -1) 中的 -1 非常重要。
*
* a:c:v1:
*
* 最后一个排除字段为空,
* 如果不使用 -1,Java 会自动丢弃末尾空字符串。
*/
String[] parts = item.split(":", -1);
String parent = parts[0];
String child = parts[1];
String version = parts[2];
Set<String> exclusions =
parseExclusions(parts.length > 3 ? parts[3] : "");
rules.computeIfAbsent(parent, k -> new ArrayList<>())
.add(new Edge(child, version, exclusions));
}
}
/*
* LinkedHashMap 有两个作用:
*
* 1. 判断组件是否已经出现,实现全局去重;
* 2. 保留组件第一次出现的顺序。
*
* value 保存第一次出现时的版本。
* 后面即使遇到同组件的其他版本,也不能覆盖。
*/
Map<String, String> selected = new LinkedHashMap<>();
// BFS 队列
Queue<Node> queue = new ArrayDeque<>();
/*
* 先按照输入顺序加入所有直属依赖。
*
* 直属依赖属于依赖树的第一层,
* 所以必须全部先输出,再处理它们的下一层依赖。
*/
if (!directLine.isEmpty()) {
String[] directItems = directLine.split("\\s+");
for (String item : directItems) {
String[] parts = item.split(":", -1);
String name = parts[0];
String version = parts[1];
Set<String> exclusions =
parseExclusions(parts.length > 2 ? parts[2] : "");
// 组件只保留第一次出现的位置和版本
if (!selected.containsKey(name)) {
selected.put(name, version);
/*
* 直属依赖中的排除项不排除直属组件本身,
* 而是从该组件的依赖子树开始生效。
*/
queue.offer(
new Node(name, version, exclusions)
);
}
}
}
/*
* BFS 分层遍历。
*
* 为什么不能 DFS?
*
* 假设:
* 项目依赖 b、a
* b -> e
* a -> c、d
*
* 题目要求:
* b a e c d
*
* BFS 正好得到这个顺序。
*/
while (!queue.isEmpty()) {
Node parent = queue.poll();
List<Edge> children =
rules.getOrDefault(
parent.name,
Collections.emptyList()
);
for (Edge edge : children) {
/*
* 如果当前依赖路径已经要求排除这个组件,
* 那么这个组件以及它下面整棵依赖子树都不能进入。
*/
if (parent.activeExclusions.contains(edge.childName)) {
continue;
}
/*
* 全局组件去重。
*
* 第一次出现的位置和版本已经确定,
* 后面再次遇到时全部忽略。
*
* 这也可以避免依赖关系中存在环时产生死循环。
*/
if (selected.containsKey(edge.childName)) {
continue;
}
// 第一次出现,确定最终版本
selected.put(edge.childName, edge.version);
/*
* 子节点的排除集合 =
* 祖先路径已经生效的排除项
* +
* 当前依赖边新增的排除项。
*
* 例如:
* a:b:v1:c
*
* 当进入 b 后,c 开始被排除。
*/
Set<String> childExclusions =
new HashSet<>(parent.activeExclusions);
childExclusions.addAll(edge.exclusions);
queue.offer(
new Node(
edge.childName,
edge.version,
childExclusions
)
);
}
}
// 按第一次出现的顺序输出
StringJoiner result = new StringJoiner(" ");
for (Map.Entry<String, String> entry : selected.entrySet()) {
result.add(
entry.getKey() + ":" + entry.getValue()
);
}
System.out.println(result);
}
/**
* 解析排除组件列表。
*
* "" -> {}
* "a" -> {a}
* "a,b,c" -> {a,b,c}
*/
private static Set<String> parseExclusions(String text) {
Set<String> set = new HashSet<>();
if (text == null || text.isEmpty()) {
return set;
}
String[] names = text.split(",");
Collections.addAll(set, names);
return set;
}
}
七、效果展示
1、输入
b:v1: a:v1:
a:c:v1: a:d:v1: b:e:v1:
2、输出
b:v1 a:v1 e:v1 c:v1 d:v1
3、说明
第1层:
b a
第2层:
b -> e
a -> c
a -> d
所以顺序必须是:b a e c d
而不能是 DFS 产生的:b e a c d

🏆下一篇:华为OD机试 - 简易内存池 - 逻辑分析(Java 新系统 200分)
🏆本专栏收录于《华为OD机试(JAVA)真题》。
刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新,全天CSDN在线答疑。

10万+

被折叠的 条评论
为什么被折叠?



