1. 引言
在实际开发中,我们经常遇到这样的场景:给定一组父子关系的节点对,要求将这些节点组织成一棵或多棵树(即森林)。这种结构常见于权限系统、组织架构、分类树等场景。
本文将介绍一种高效的算法,用于将扁平的父子关系列表转换为树形结构,并找出所有根节点。
2. 问题描述
我们收到一组数据,每项是一个 (child_id, parent_id)
的结构,表示一个子节点和它的父节点。这些数据可能是无序的,例如 (2, 4)
可能在 (1, 2)
前面出现。
我们的目标是:
- 将这些节点组织成一棵或多棵树(森林)
- 找出所有根节点(没有父节点的节点)
这些 ID 可以是整数、字符串,甚至更复杂的数据结构,因此我们的算法应具备通用性。
例如,下面是一个树的结构:
其中 (2, 4)
表示 2 是 4 的父节点。
3. 解决方案
一个直观的思路是:
- 遍历每对
(child_id, parent_id)
- 找到或创建对应的父节点和子节点
- 将子节点挂载到父节点下
- 维护一个
roots
集合,用于记录当前所有可能的根节点
3.1 算法逻辑
我们使用一个辅助哈希表来快速查找或创建节点,同时维护一个集合来记录所有可能的根节点。
伪代码如下:
algorithm BuildingForestFromFlatList(list):
roots <- an empty set
hash_table <- create an empty hash table
for (child_id, parent_id) in list:
child_node <- FIND_OR_CREATE(child_id, hash_table)
parent_node <- FIND_OR_CREATE(parent_id, hash_table)
parent_node.children <- parent_node.children + {child_node}
child_node.parent <- parent_node
hash_table[child_id] <- child_node
hash_table[parent_id] <- parent_node
if parent_node not in roots:
roots <- roots + {parent_node}
if child_node in roots:
roots <- roots - {child_node}
return roots
algorithm FIND_OR_CREATE(id, hash_table):
if id in hash_table.keys:
node <- hash_table[id]
else:
node <- create an empty node
node.id <- id
return node
3.2 正确性证明
我们维护一个 roots
集合,表示当前所有“尚未发现父节点”的节点(即候选根节点)。
- 每当发现一个节点有父节点时,就将它从
roots
中移除 - 每次创建一个新节点时,如果它没有父节点,则加入
roots
最终,roots
中只剩下真正的根节点。
3.3 时间复杂度分析
- 最坏情况:不使用哈希表,每次查找节点都需要遍历整个树结构,时间复杂度为 O(n²)
- 平均情况:使用哈希表,每次查找时间为 O(1),总时间复杂度为 O(n)
✅ 建议:务必使用哈希表,否则在节点数较大时会显著影响性能
4. 图的连通分量视角(DFS 解法)
我们可以将这个问题转化为图论问题:
- 每个节点是一个图的顶点
- 每个
(child_id, parent_id)
是一条边 - 整个结构可能由多个连通分量组成(即森林)
我们可以通过深度优先搜索(DFS)找出所有连通分量,并在每个连通分量中找到根节点(即没有父节点的节点)。
4.1 伪代码实现
algorithm BuildForestFromFlatList(list):
graph <- initialize an empty graph
for (child_id, parent_id) in list:
if child_id not in graph.nodes:
graph.nodes <- add child_id to graph.nodes
if parent_id not in graph.nodes:
graph.nodes <- add parent_id to graph.nodes
graph.nodes[child_id].parent <- graph.nodes[parent_id]
graph.nodes[parent_id].children.add(graph.nodes[child_id])
roots <- an empty set
for node in graph.nodes:
if node.visited is false:
while node.parent != NONE:
node <- node.parent
roots <- roots + {node}
DepthFirst(node)
return roots
其中 DepthFirst
是标准的 DFS 实现:
algorithm DepthFirst(node):
node.visited <- true
for child in node.children:
DepthFirst(child)
4.2 复杂度分析
- 使用邻接表存储结构时,DFS 的复杂度为 O(n)
- 使用邻接矩阵则为 O(n²)
✅ 建议:优先使用邻接表结构,避免性能瓶颈
4.3 极端情况处理
- 如果输入为空列表,表示没有节点,算法应返回空集合
- 如果只有一个节点没有父子关系,该节点即为根节点
⚠️ 注意:输入中不能有环,否则会进入死循环
5. 总结
我们介绍了两种构建森林的算法:
方法 | 核心思想 | 时间复杂度 | 是否推荐 |
---|---|---|---|
哈希表 + 集合 | 逐个处理节点,维护候选根集合 | O(n) | ✅ 推荐 |
DFS 遍历 | 图的连通分量视角 | O(n) | ✅ 推荐 |
两种方法都可在 O(n) 时间内完成,前提是使用合适的数据结构。
在实际开发中,建议优先使用第一种方法(哈希表 + 集合),因为它实现更简单,逻辑更清晰,也更容易扩展和维护。