Performance Analysis of Hierarchical Tree Construction Algorithms in Java

Database Schema and Test Data Preparation

To evaluate the processing efficiency of tree structures at scale, a relational model is established first.

Table Structure Definition

The schema utilizes a self-referencing foreign key to denote parent-child relationships.

CREATE TABLE system_organization (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `parent_id` bigint DEFAULT NULL,
  `org_name` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Bulk Data Generation

A stored procedure iterates one million times. It creates a linear dependency where every 100th ID acts as a parent for the preceding 99 IDs. The initial record has no parent reference.

DELIMITER $$
CREATE PROCEDURE SeedOrganizationData()
BEGIN
  DECLARE counter INT DEFAULT 1;
  WHILE counter <= 1000000 DO
    INSERT INTO system_organization (parent_id, org_name) 
    VALUES (
      IF(MOD(counter, 100) = 1, NULL, counter - MOD(counter, 100)), 
      CONCAT('Org ', counter)
    );
    SET counter = counter + 1;
  END WHILE;
END$$
DELIMITER ;
CALL SeedOrganizationData();

Java Implementation Strategies

We utilize Spring Boot with a standard ORM framework. The entity class separates persistent fields from transient tree associations.

Domain Model

public class TreeNode {
    private Long nodeId;
    private Long parentId;
    private String name;
    
    @Transient
    private List<TreeNode> subNodes;
    // Getters and Setters omitted for brevity
}

Approach A: Stream Collection Grouping

This method relies on parallel stream operations to group children by their parent ID immediately after fetching all records.

@GetMapping("/strategy-stream")
public List<TreeNode> assembleByStream() {
    List<TreeNode> rawData = repository.findAll();
    
    Map<Long, List<TreeNode>> hierarchyMap = rawData.stream()
        .collect(Collectors.groupingBy(TreeNode::getParentId));
    
    return rawData.stream()
        .filter(node -> node.getParentId() == null || node.getParentId().equals(0L))
        .peek(node -> {
            List<TreeNode> children = hierarchyMap.get(node.getNodeId());
            if (children != null) node.setSubNodes(children);
        })
        .collect(Collectors.toList());
}

Approach B: Iterative Hash Map Lookup

A traditional loop-based approach that populates maps before traversing the list to atttach references.

@GetMapping("/strategy-loop")
public List<TreeNode> assembleByLoop() {
    List<TreeNode> rawData = repository.findAll();
    Map<Long, List<TreeNode>> indexMap = new HashMap<>();
    List<TreeNode> rootList = new ArrayList<>();

    for (TreeNode node : rawData) {
        indexMap.computeIfAbsent(node.getParentId(), k -> new ArrayList<>())
                .add(node);
        if (node.getParentId() == null) rootList.add(node);
    }

    for (TreeNode node : rawData) {
        node.setSubNodes(indexMap.get(node.getNodeId()));
    }

    return rootList;
}

Approach C: Third-Party Utility Library

Utilizing an external library abstraction to handle the tree construction logic automatically.

import cn.hutool.core.tree.TreeUtil;
import cn.hutool.core.tree.TreeNode;

@GetMapping("/strategy-lib")
public List<? extends TreeNode> assembleWithLibrary() {
    List<DeptDTO> depts = repository.findAll();
    List<TreeStructureNode> structureList = new ArrayList<>();

    for (DeptDTO dto : depts) {
        TreeStructureNode temp = new TreeStructureNode();
        temp.setId(dto.getNodeId());
        temp.setParentId(dto.getParentId());
        temp.setName(dto.getName());
        structureList.add(temp);
    }
    return TreeUtil.build(structureList, 0L);
}

Approach D: Recursive Traversal

This strategy employs Depth First Search logic to traverse parents down to descendents, requiring efficient child lookup.

@GetMapping("/strategy-recursive")
public List<TreeNode> assembleRecursively() {
    List<TreeNode> allNodes = repository.findAll();
    Map<Long, List<TreeNode>> cache = allNodes.stream()
            .collect(Collectors.groupingBy(n -> n.getParentId() != null ? n.getParentId() : 0));
    
    List<TreeNode> result = new ArrayList<>();
    for (TreeNode node : allNodes) {
        if (node.getParentId() == null) {
            recursivePopulate(node, cache);
            result.add(node);
        }
    }
    return result;
}

private void recursivePopulate(TreeNode currentNode, Map<Long, List<TreeNode>> cache) {
    List<TreeNode> children = cache.get(currentNode.getNodeId());
    if (children == null) return;
    currentNode.setSubNodes(children);
    for (TreeNode child : children) {
        recursivePopulate(child, cache);
    }
}

Performance Observations

Execution times vary based on system load and JVM warmup, but trends remain consistent across dataset sizes ranging from 10k to 1M nodes.

  1. Low Volume (10,000 Records): All four methods execute within acceptable latency windows under 100ms. Differences are negligible for small datasets.
  2. Medium Volume (50,000 - 100,000 Records): The iterative and stream approaches maintain stability. The library utility remains competitive due to optimized internal caching. Recursive solutions begin showing noticeable overhead due to function call stack depth.
  3. High Volume (> 1 Million Records): Memory pressure increases significantly. The recursive implementation consumes more stack space and CPU cycles per node compared to the flat-map approaches. Stream grouping generally offers the best balance between readability and throughput for massive payloads. The pure recursive method without pre-caching becomes impractical at this scale.

Tags: java tree traversal Performance Benchmark Spring Boot Algorithm Efficiency

Posted on Mon, 12 Oct 2026 16:52:12 +0000 by suspect