前言
项目中经常会遇到前端需要展现树形结构数据,比如菜单树、省市区联动,在小数据量的时候,不管用什么算法都可以,但一旦数据大,不同算法的差距就会非常的大。公司的项目中老代码用的是递归方法构建树结构,2万多个数据就需要跑20s,把生产服务器CPU都跑满了,于是对该方法进行重构。
思路
- 为了不影响传入的数据,需要拷贝原数据集合。(增加和删除频繁,使用
LinkedList
结构) - 拷贝的过程中,一并收集每个节点的id,后面用来判断每个节点的
parentId
是否存在,不存在则认为是根节点。(为了在比较的时候更快速,使用Set
结构) - 将集合中的数据用
parentId
作为key
,放入map
中,得到Map<String, List<TreeNode>>
结构数据。(利用Java8的Collectors.groupingBy
获取) - 遍历集合,将节点的
id
作为key,从第3步组装的map中获取子节点,利用Java的地址引用构建树,并且将不是根节点的数据从集合移出。
实现代码 (可以直接运行)
TreeNode
@Data
public class TreeNode {
private String id;
private String code;
private String name;
private String parentId;
private List<TreeNode> children;
}
实现代码
public static List<TreeNode> buildTree(List<Map<String, Object>> oriDataMapList) {
// 判空
if (oriDataMapList == null || oriDataMapList.isEmpty()) {
return new ArrayList<>();
}
// 最终要返回的只保留根节点的集合,增删比较多,使用LinkedList
List<TreeNode> respTreeNodeList = new LinkedList<>();
// id集合,后面用来判断parentId是否在oriTreeNodeList存在,如果存在则说明是子节点,不存在则认为是根节点
Set<String> idSet = new HashSet<>(oriDataMapList.size());
// 拷贝数据
TreeNode treeNode;
for (Map map : oriDataMapList) {
treeNode = new TreeNode();
treeNode.setId((String) map.get("id"));
treeNode.setCode((String) map.get("code"));
treeNode.setName((String) map.get("name"));
treeNode.setParentId((String) map.get("parentId"));
// 遍历的时候顺便保存id集合
idSet.add(treeNode.getId());
respTreeNodeList.add(treeNode);
}
// 获取由parentId作为key的map,同一个父节点的数据已经被汇总成一个list,通过key就能获取
Map<String, List<TreeNode>> parentIdMap = respTreeNodeList.stream().collect(Collectors.groupingBy(item -> item.getParentId() != null ? item.getParentId() : "nonParent"));
// 设置children,并从根节点移出不是父节点的数据
Iterator<TreeNode> iterator = respTreeNodeList.iterator();
TreeNode node;
while (iterator.hasNext()) {
node = iterator.next();
// 获取当前结点的子节点集合
node.setChildren(parentIdMap.get(node.getId()));
// 如果父id在id集合中存在,则说明他不是根节点,移除
if (idSet.contains(node.getParentId())) {
iterator.remove();
}
}
return respTreeNodeList;
}
测试代码
public static void main(String[] args) {
// 模拟测试数据
List<Map<String, Object>> mapList = new ArrayList<>();
Map<String, Object> map;
for (int i = 0; i < 1000; i++) {
String parentId = String.valueOf(UUID.randomUUID());
map = new HashMap<>();
map.put("id", parentId);
// %03d 长度为3,不够的补0
map.put("code", String.format("%03d", i));
mapList.add(map);
for (int j = 0; j < 1000; j++) {
map = new HashMap<>();
String id = String.valueOf(UUID.randomUUID());
map.put("id", id);
map.put("code", String.format("%03d", i) + String.format("%03d", j));
map.put("parentId", parentId);
mapList.add(map);
}
}
long start = System.currentTimeMillis();
List<TreeNode> treeNodeList = buildTree(mapList);
String msg = String.format("组装【%s】条数据,耗时【%s】ms", mapList.size(), (System.currentTimeMillis() - start));
System.out.println(msg);
}
运行结果
组装【1001000】条数据,耗时【417】ms
结论:
可以看到,跑一百万的数据耗时只有不到0.5s,速度大大优化