Java 通用树形结构构建与解析工具类解析

7 阅读2分钟

引言

在后端开发中,我们经常需要处理层级数据,比如部门结构、菜单权限或商品分类。数据库通常存储为扁平的列表(List),而前端往往需要嵌套的树形结构(Tree)。

探讨一种基于HashMap的高效算法,实现List到Tree以及Tree到List的双向转换。

场景与模型定义

两种数据结构:

  • Node(扁平节点) :包含自身ID和父ID,通常对应数据库表结构。
  • Tree(树形节点) :包含自身ID和子节点列表。
// 扁平节点
class Node {
    int id;
    int pid;
    // 构造函数与Getter省略
}

// 树形节点
class Tree {
    int id;
    List<Tree> children;
    // 构造函数与Getter省略
}

核心算法实现

List转Tree

核心思路是利用HashMap的空间换时间策略,将时间复杂度降低到O(n)。

  • 第一步:遍历列表,将所有节点存入Map,Key为节点ID。同时,我们需要找到根节点的ID(即最小的pid)。
  • 第二步:再次遍历列表,通过Map快速查找父节点,将当前节点加入父节点的children列表。
public static Tree toTree(List<Node> nodes) {

    if (nodes == null || nodes.isEmpty()) {
        return null;
    }

    // 1. 初始化Map并寻找根节点ID
    var map = new HashMap<Integer, Tree>(nodes.size() + 1);
    var rid = Integer.MAX_VALUE;

    for (var n : nodes) {
        if (n == null) {
            continue;
        }

        // 寻找最小的pid作为根节点ID
        if (n.getPid() < rid) {
            rid = n.getPid();
        }

        // 将节点放入Map
        map.put(n.getId(), new Tree(n.getId()));
    }

    if (map.isEmpty()) {
        return null;
    }

    // 确保根节点也在Map中
    map.putIfAbsent(rid, new Tree(rid));
    var root = map.get(rid);

    // 2. 组装树形结构
    for (var n : nodes) {
        if (n == null) {
            continue;
        }

        var child = map.get(n.getId());
        var parent = map.get(n.getPid());

        if (parent != null) {
            parent.getChildren().add(child);
        }
    }

    return root;
}

Tree转List

这是一个典型的深度优先搜索(DFS)过程,通过递归遍历树的每一层。

public static List<Node> toNodes(Tree tree) {

    var data = new ArrayList<Node>();
    if (tree == null) {
        return data;
    }

    appendChildren(data, tree);

    return data;
}

private static void appendChildren(List<Node> data, Tree tree) {

    var children = tree.children;

    if (children.isEmpty()) {
        return;
    }

    // 遍历子节点,将其转换为扁平Node并加入列表
    for (var c : children) {
        data.add(new Node(c.id, tree.id));
        // 递归处理下一级
        appendChildren(data, c);
    }
}

测试与验证

通过一个具体的树形结构来验证代码的正确性。

测试数据

  • 根节点:0
  • 一级子节点:1, 2(父节点为0)
  • 二级子节点:3, 4(父节点为1)

代码结构

      0
     / \
    1   2
   / \
  3   4

运行结果

  • 转Tree
{
  "id": 0,
  "children": [
    {
      "id": 1,
      "children": [
        {
          "id": 3,
          "children": []
        },
        {
          "id": 4,
          "children": []
        }
      ]
    },
    {
      "id": 2,
      "children": []
    }
  ]
}
  • 转List
[
  {
    "id": 1,
    "pid": 0
  },
  {
    "id": 3,
    "pid": 1
  },
  {
    "id": 4,
    "pid": 1
  },
  {
    "id": 2,
    "pid": 0
  }
]

总结

  • 时间复杂度:toTree方法通过两次遍历,时间复杂度为O(n),优于嵌套循环的O(n²)算法。
  • 空间复杂度:使用了一个HashMap存储节点引用,空间复杂度为O(n)。
  • 适用性:该算法适用于内存中处理中等规模的数据集构建层级关系