常见的树形结构封装
在日常的开发工作中,时常会遇到树形结构的封装,比如:树形结构的菜单数据、部门数据等等。最近工作中,指标的树形结构封装场景频繁,比如:校验每个层级的指标权重之和要等于100,指标的满树校验等,接下来我们就来看一下我的思路。
一、准备数据
(1)准备一个指标实体类
@Data public class Indicator { private String code; private String parentCode; private String name; private Integer weight; private List<Indicator> children = Lists.newArrayList(); public Indicator() { } public Indicator(String code, String parentCode, String name, Integer weight) { this.code = code; this.parentCode = parentCode; this.name = name; this.weight = weight; } }
(2)树形结构封装代码【并提供几个重载方法】
import com.beust.jcommander.internal.Lists; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import javax.annotation.Nonnull; import java.lang.reflect.Field; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; /** * @author niubi */ @Slf4j public class TreeUtil { /** * 组装树结构 * * @param list . * @param fieldNameForIndex 主键名称 * @param fieldNameForParentIndex parent主键名称 * @param fieldNameForChildren children名称 * @return . */ @SuppressWarnings(value = {"rawtypes", "unchecked"}) public static Optional<List> treeify(@Nonnull List list, @Nonnull String fieldNameForIndex, @Nonnull String fieldNameForParentIndex, @Nonnull String fieldNameForChildren) { Objects.requireNonNull(list, "list is null"); Objects.requireNonNull(fieldNameForIndex, "fieldNameForIndex is null"); Objects.requireNonNull(fieldNameForChildren, "fieldNameForChildren is null"); Objects.requireNonNull(fieldNameForParentIndex, "fieldNameForParentIndex is null"); List root = new ArrayList(); Map map = new HashMap(); try { for (Object o : list) { Field indexField = o.getClass().getDeclaredField(fieldNameForIndex); if (!indexField.isAccessible()) { indexField.setAccessible(true); } Object index = indexField.get(o); map.put(index, o); } for (Object o : map.keySet()) { Object obj = map.get(o); Class<?> clazz = obj.getClass(); Field parentIndexField = clazz.getDeclaredField(fieldNameForParentIndex); if (!parentIndexField.isAccessible()) { parentIndexField.setAccessible(true); } Object parentIndex = parentIndexField.get(obj); Object parent = map.get(parentIndex); if (Objects.isNull(parent)) { root.add(obj); continue; } Class<?> clazz1 = parent.getClass(); Field childrenField = null; try { childrenField = clazz1.getDeclaredField(fieldNameForChildren); } catch (Exception e) { childrenField = clazz1.getSuperclass().getDeclaredField(fieldNameForChildren); } if (!childrenField.isAccessible()) { childrenField.setAccessible(true); } List children = (List) childrenField.get(parent); if (Objects.isNull(children)) { children = new ArrayList(); } children.add(obj); childrenField.set(parent, children); } } catch (Exception e) { log.error(e.getMessage(), e); root = null; } return Optional.ofNullable(root); } @SuppressWarnings(value = {"rawtypes"}) public static Optional<List> treeify(@Nonnull List list) { return treeify(list, "id", "parentId", "children"); } @SuppressWarnings(value = {"rawtypes"}) public static Optional<List> treeify(@Nonnull List list, @Nonnull String fieldNameForIndex) { return treeify(list, fieldNameForIndex, "parentId", "children"); } @SuppressWarnings(value = {"rawtypes"}) public static Optional<List> treeify(@Nonnull List list, @Nonnull String fieldNameForIndex, @Nonnull String fieldNameForParentIndex) { return treeify(list, fieldNameForIndex, fieldNameForParentIndex, "children"); } }
(3)指标树形结构代码执行
public static void testTree1() { List<Indicator> indicators = Lists.newArrayList(); indicators.add(new Indicator("1", "0", "一级指标1", 50)); indicators.add(new Indicator("11", "1", "二级指标1", 100)); indicators.add(new Indicator("111", "11", "三级指标1", 100)); indicators.add(new Indicator("2", "0", "一级指标2", 50)); indicators.add(new Indicator("21", "2", "二级指标1", 50)); indicators.add(new Indicator("221", "2", "二级指标2", 50)); // 封装树形结构 Optional<List> treeify = TreeUtil.treeify(indicators, "code", "parentCode", "children"); List<Indicator> indicatorTree = Lists.newArrayList(); if (treeify.isPresent()) { indicatorTree = treeify.get(); } System.out.println("指标树形结构:" + indicatorTree); }
二、校验指标树形结构中任意节点下的子节点权重之和为 100
(1)方法一:使用递归方式进行逐层校验
public static void testTree3() { List<Indicator> indicators = Lists.newArrayList(); indicators.add(new Indicator("1", "0", "一级指标1", 50)); indicators.add(new Indicator("11", "1", "二级指标1", 100)); indicators.add(new Indicator("111", "11", "三级指标1", 100)); indicators.add(new Indicator("2", "0", "一级指标2", 50)); indicators.add(new Indicator("21", "2", "二级指标1", 50)); indicators.add(new Indicator("221", "2", "二级指标2", 50)); List<Indicator> oneIndicators = indicators.stream().filter(user -> user.getParentCode().equals("0")).collect(Collectors.toList()); Integer sum = oneIndicators.stream().map(Indicator::getWeight).reduce((x, y) -> x + y).get(); if (sum != 100) { System.out.println("一级指标权重之和校验失败"); return; } // 逐个一级指标递归校验权重之和为 100 for (Indicator user : oneIndicators) { Boolean flag = validData(user, indicators); if (!flag) { System.out.println("校验失败"); return; } } System.out.println("校验成功"); } private static Boolean validData(Indicator indicator, List<Indicator> indicators) { // 过滤出当前指标的下级指标集合 List<Indicator> children = indicators.stream().filter(element -> element.getParentCode().equals(indicator.getCode())).collect(Collectors.toList()); if (CollectionUtils.isEmpty(children)) { return true; } Integer sum = children.stream().map(Indicator::getWeight).reduce((x, y) -> x + y).get(); if (sum != 100) { System.out.println("code = " + indicator.getCode() + ", parentCode = " + indicator.getParentCode() + ", 校验失败"); return false; } for (Indicator child : children) { validData(child, indicators); } return true; }
(2)方法二:使用非递归方式逐层校验
public static void testTree2() { List<Indicator> indicators = Lists.newArrayList(); indicators.add(new Indicator("1", "0", "一级指标1", 50)); indicators.add(new Indicator("11", "1", "二级指标1", 100)); indicators.add(new Indicator("111", "11", "三级指标1", 100)); indicators.add(new Indicator("2", "0", "一级指标2", 50)); indicators.add(new Indicator("21", "2", "二级指标1", 50)); indicators.add(new Indicator("221", "2", "二级指标2", 50)); // 封装树形结构 Optional<List> treeify = TreeUtil.treeify(indicators, "code", "parentCode", "children"); List<Indicator> userTree = Lists.newArrayList(); if (treeify.isPresent()) { userTree = treeify.get(); } // 判断每一个节点下所有子节点的分数累加之和是否是100 Boolean flag = validAgeSum(userTree); if (flag) { System.out.println("校验成功"); } else { System.out.println("校验失败"); } } private static Boolean validAgeSum(List<Indicator> userTree) { List<Indicator> children = userTree; // 顶层一级指标校验 Integer sum1 = children.stream().map(Indicator::getWeight).reduce((x, y) -> x + y).get(); if (sum1 != 100) { return false; } // 非递归逐层每个根节点校验 while(CollectionUtils.isNotEmpty(children)) { for (Indicator indicator : children) { if (CollectionUtils.isEmpty(indicator.getChildren())) { return true; } Integer sum2 = indicator.getChildren().stream().map(Indicator::getWeight).reduce((x, y) -> x + y).get(); if (sum2 != 100) { System.out.println("code = " + indicator.getCode() + ", parentCode = " + indicator.getParentCode() + ", 校验失败"); return false; } } // 重新组装子节点集合 children = children.stream().map(Indicator::getChildren).flatMap(Collection::stream).collect(Collectors.toList()); } return true; }
三、校验指标树形结构是否是一棵满树【满树:以任意一个一级指标节点开始,树的高度等同】
/** * 封装树形结构 */ public static void testTree1() { List<Indicator> indicators = Lists.newArrayList(); indicators.add(new Indicator("1", "0", "一级指标1", 50)); indicators.add(new Indicator("11", "1", "二级指标1", 100)); indicators.add(new Indicator("111", "11", "三级指标1", 100)); indicators.add(new Indicator("2", "0", "一级指标2", 50)); indicators.add(new Indicator("21", "2", "二级指标1", 50)); indicators.add(new Indicator("211", "21", "san级指标1", 100)); indicators.add(new Indicator("221", "2", "二级指标2", 50)); indicators.add(new Indicator("222", "221", "三级指标1", 100)); // 封装树形结构 Optional<List> treeify = TreeUtil.treeify(indicators, "code", "parentCode", "children"); List<Indicator> indicatorTree = Lists.newArrayList(); if (treeify.isPresent()) { indicatorTree = treeify.get(); } System.out.println("指标树形结构:" + indicatorTree); // 判断树是否是等高 Boolean isFullTree = isFullTree(indicatorTree); System.out.println(isFullTree); } private static Boolean isFullTree(List<Indicator> userTree) { List<Indicator> children = userTree; while(CollectionUtils.isNotEmpty(children)) { // 获取所有根节点下的子节点列表 List<List<Indicator>> lists = children.stream() .map(Indicator::getChildren) .map(userList -> Objects.isNull(userList) ? new ArrayList<Indicator>() : userList) .collect(Collectors.toList()); // 获取所有根节点下的子节点非空数量 long count = lists.stream().filter(CollectionUtils::isNotEmpty).count(); if (count != 0 && count < children.size()) { // 去除特殊情况:最后一层叶子节点【count = 0】 return false; } children = lists.stream().flatMap(Collection::stream).collect(Collectors.toList()); } return true; }
推荐:hutool 工具类也是一款非常值得开发者去研究和借鉴,树形结构的封装也是很完美,而且还可以自行扩展字段信息;