常見的樹形結構封裝


  在日常的開發工作中,時常會遇到樹形結構的封裝,比如:樹形結構的菜單數據、部門數據等等。最近工作中,指標的樹形結構封裝場景頻繁,比如:校驗每個層級的指標權重之和要等於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 中樹結構封裝文檔

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM