常见的树形结构封装

  在日常的开发工作中,时常会遇到树形结构的封装,比如:树形结构的菜单数据、部门数据等等。最近工作中,指标的树形结构封装场景频繁,比如:校验每个层级的指标权重之和要等于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 工具类也是一款非常值得开发者去研究和借鉴,树形结构的封装也是很完美,而且还可以自行扩展字段信息;

  1)hutool 工具类官方文档

  2)hutool 中树结构封装文档

原文地址:https://www.cnblogs.com/blogtech/p/14129014.html