1. 引言

在实际开发中,我们经常遇到这样的场景:给定一组父子关系的节点对,要求将这些节点组织成一棵或多棵树(即森林)。这种结构常见于权限系统、组织架构、分类树等场景。

本文将介绍一种高效的算法,用于将扁平的父子关系列表转换为树形结构,并找出所有根节点。

2. 问题描述

我们收到一组数据,每项是一个 (child_id, parent_id) 的结构,表示一个子节点和它的父节点。这些数据可能是无序的,例如 (2, 4) 可能在 (1, 2) 前面出现。

我们的目标是:

  • 将这些节点组织成一棵或多棵树(森林)
  • 找出所有根节点(没有父节点的节点)

这些 ID 可以是整数、字符串,甚至更复杂的数据结构,因此我们的算法应具备通用性。

例如,下面是一个树的结构:

tree

其中 (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) 时间内完成,前提是使用合适的数据结构。

在实际开发中,建议优先使用第一种方法(哈希表 + 集合),因为它实现更简单,逻辑更清晰,也更容易扩展和维护。


原始标题:From Lists to Forests