List<T> 转 树形结构

2022-07-29 13:35:34

百度百科递归说明:

程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

前言:

在系统开发过程中,可能会碰到一些需求,需要构建树形结构,数据库一般就使用父id来表示,比如构建菜单、构建地区级联、构建部门层级结构等等。虽然可以通过数据库SQL查询,但我们一般都是通过SQL一次性查询出所有数据,在程序中处理成树形结构。本文讲述如何将一个List<T> 处理成想要的树形结构。

1、对象

@Data
public class Tree {
    // id
    private Integer id;
    // 文件夹名称
    private String name;
    // 文件夹下的文件数量
    private Integer count;
    // 父文件夹ID
    private Integer parentId;
    // 子文件夹对象
    private List<Tree> children;
}

最容易想到的树形其实就是我们操作系统的文件夹,我们以文件夹为例:

2、list转树形结构

按照上述文件夹结构,模拟数据库查询出来的对象列表,构建测试用的数据:

// 最顶级的文件夹目录,parentId = 0
List<Tree> treeList = Arrays.asList(new Tree(1, "A", 2, 0, null),
                                    new Tree(2, "B", 1, 1, null),
                                    new Tree(3, "C", 0, 1, null),
                                    new Tree(4, "D", 1, 2, null),
                                    new Tree(5, "E", 1, 2, null),
                                    new Tree(6, "F", 0, 0, null),
                                    new Tree(7, "G", 1, 0, null)
                                   );

这里根据Java的版本,提供三种方法将list 转成树形:

1、for 方法转树形

public class TreeTest {
    public static void main(String[] args) {
        List<Tree> node = forMethod(treeList);
        System.out.println(node);
    }

    /**
     * 双重for循环方法转换成树形结构
     * @param treeList
     * @return
     */
    public static List<Tree> forMethod(List<Tree> treeList) {
        List<Tree> rootTree = new ArrayList<>();
        for (Tree tree : treeList) {
            // 第一步 筛选出最顶级的父节点
            if (0 == tree.getParentId()) {
                rootTree.add(tree);
            }
            // 第二步 筛选出该父节点下的所有子节点列表 
            for (Tree node : treeList) {
                if (node.getParentId().equals(tree.getId())) {
                    if (CollectionUtils.isEmpty(tree.getChildren())) {
                        tree.setChildren(new ArrayList<>());
                    }
                    tree.getChildren().add(node);
                }
            }
        }
        return rootTree;
    }
}

2、递归方法转树形

public class TreeTest {
    public static void main(String[] args) {
        List<Tree> node = recursionMethod(treeList);
        System.out.println(node);
    }
  
    /**
     * 递归方法转换成树形结构
     * @param treeList
     * @return
     */
    public static List<Tree> recursionMethod(List<Tree> treeList) {
        List<Tree> trees = new ArrayList<>();
        for (Tree tree : treeList) {
            // 找出父节点
            if (0 == tree.getParentId()) {
                // 调用递归方法填充子节点列表
                trees.add(findChildren(tree, treeList));
            }
        }
        return trees;
    }

    /**
     * 递归方法
     * @param tree 父节点对象
     * @param treeList 所有的List
     * @return
     */
    public static Tree findChildren(Tree tree, List<Tree> treeList) {
        for (Tree node : treeList) {
            if (tree.getId().equals(node.getParentId())) {
                if (tree.getChildren() == null) {
                    tree.setChildren(new ArrayList<>());
                }
                // 递归 调用自身
                tree.getChildren().add(findChildren(node, treeList));
            }
        }
        return tree;
    }
}

3、stream方法转树形

public class TreeTest {
    public static void main(String[] args) {
        List<Tree> node = recursionMethod(treeList);
        System.out.println(node);
    }
  
    /**
     * stream方法转换成树形结构
     * @param treeList
     * @return
     */
    public static List<Tree> streamMethod(List<Tree> treeList) {
        List<Tree> list = treeList.stream()
                                  // 筛选出父节点
                                  .filter(t -> t.getParentId() == 0)
                                  // 设置父节点的子节点列表
                                  .map(item -> {item.setChildren(streamGetChildren(item, treeList)); return item;})
                                  .collect(Collectors.toList());
        return list;
    }

    /**
     * stream 方式递归查找子节点列表
     * @return
     */
    public static List<Tree> streamGetChildren(Tree tree, List<Tree> treeList) {
        List<Tree> list = treeList.stream()
                                  .filter(t -> t.getParentId().equals(tree.getId()))
                                  .map(item -> {item.setChildren(streamGetChildren(item, treeList)); return item;})
                                  .collect(Collectors.toList());
        return list;
    }
}

总结

        上面三种方法转树形可以看出,他们其实都是一样的套路,不管写法如何,核心思路都是不变的,我们可以总结出以下规律:

1、找出所有的根父节点

2、通过父节点对比整个集合列表,找出父节点下的所属子节点(此处需要递归,不断自己调自己,一直把所有的父节点筛选完并设置好父节点下的子节点列表)

3、组装成树形List 返回。

可以看到,其实在 stream 转树形的方法中,streamMethodstreamGetChildren 方法存在类似的部分:

  • 每次都要筛选对比父节点;

  • 每次都要设置父节点的子节点列表,

而且存在 根父节点ID 写死的问题,无法灵活应用,我们抽取出公共部分,可以优化成这样。

4、stream 转树形优化

// 第一种优化,我们合并上述两个方法的相同部分
public class TreeTest {
    public static void main(String[] args) {
        List<Tree> node = streamMethod(0, treeList);
        System.out.println(node);
    }

    /**
     * stream 方法转换树形结构方法的优化
     * @param parentId
     * @param treeList
     * @return
     */
    public static List<Tree> streamMethod(Integer parentId, List<Tree> treeList) {
        List<Tree> list = treeList.stream()
                // 筛选父节点
                .filter(t -> t.getParentId().equals(parentId))
                // 递归设置子节点
                .map(item -> {
                    item.setChildren(streamMethod(item.getId(), treeList));
                    return item;
                })
                .collect(Collectors.toList());
        return list;
    }
}
// 第二种优化,只是写法的不同,核心思路不变
public class TreeTest {
    public static void main(String[] args) {
        List<Tree> node = streamMethod(0, treeList);
        System.out.println(node);
    }
  
    /**
     * stream 方法转换树形结构方法的优化
     * @param parentId
     * @param treeList
     * @return
     */
    public static List<Tree> streamMethod(Integer parentId, List<Tree> treeList) {
        List<Tree> list = new ArrayList<>();
        Optional.ofNullable(treeList).orElse(new ArrayList<>())
                .stream()
                // 第一次筛选出主父节点列表进入循环,循环里面 进入递归 筛选出递归传递的从父节点列表
                .filter(root -> root.getParentId().equals(parentId))
                // 递归,最末的父节点从整个列表筛选出它的子节点列表依次组装
                .forEach(tree -> {
                    List<Tree> children = streamMethod(tree.getId(), treeList);
                    tree.setChildren(children);
                    list.add(tree);
                });
        return list;
    }
}

3、树形结构子节点属性值累加到父节点

经过上面的代码,我们已经成功的将 List<T> 转换成了我们想要的树形结构了,但是此时有需求,要求我们文件夹名称后面附带文件夹包含的文件数量,

数据库或者构造的集合数据中的文件数量只统计了文件夹本身下面的文件数量,并没有加上子文件夹里面的所有文件夹数量。我们需要将子文件夹的 count 属性逐

级累加到父节点的 count 属性中更新它。

public class TreeTest {
    public static void main(String[] args) {
        // treeList 是已经处理好的 树形结构 集合
        countHandler(treeList);
        System.out.println(treeList);
    }

    /**
     * 递归将子节点属性值累加给父节点
     * @param treeList
     * @return
     */
    private int countHandler(List<Tree> treeList) {
        int count = 0;
        if(CollectionUtil.isEmpty(treeList)){
            return count;
        }
        for (Tree tree : treeList) {
            count += tree.getCount();
            if (CollectionUtil.isEmpty(tree.getChildren())) {
                continue;
            }
            count += countHandler(tree.getChildren());
            tree.setCount(count);
            if (tree.getParentId() == null || tree.getParentId() == 0) {
                count = 0;
            }
        }
        return count;
    }
}
  • 作者:悲凉的秋风
  • 原文链接:https://blog.csdn.net/qq_34371461/article/details/118438754
    更新时间:2022-07-29 13:35:34