树结构分页方案

背景

需要实现一个树结构的查询,分页,过滤功能。

难点

1.如果按照全数据分页,会出现一棵树在2个页上,出现断层。

2.过滤后,最好的体验是节点的上级节点需要展示。

方案思路

1.只对顶层节点进行分页。

2.递归的去过滤树节点。

实现

获取列表数据

此时树列表数据对应的是数据库的一行行记录,最小化粒度的获取到树的列表数据,方便后续的递归。

树转化

列表数据转化为树结构。

/**
     * 列表转树
     *
     * @param list     节点列表
     * @param parentId 根id
     * @return java.util.List<com.skuu.math.tree.page.Tree>
     * @author dcx
     * @date 2024/3/4 18:04
     **/
    public static List<Tree> listToTree(List<Tree> list, Long parentId) {
        Map<Long, Tree> idMap = new HashMap<>();
        List<Tree> rootData = new ArrayList<>();
        for (Tree item : list) {
            //1.遍历找到顶层节点
            if (item.getPid().equals(parentId)) {
                rootData.add(item);
            }
            //2.空间换时间,将tree缓存起来
            idMap.put(item.getId(), item);
        }
        for (Tree item : list) {
            Long tmpParentId = item.getPid();
            // 3.排除根节点
            if (tmpParentId.equals(parentId)) {
                continue;
            }
            //4.没有根节点的,不显示。
            Tree parent = idMap.get(tmpParentId);
            if (parent == null) {
                continue;
            }
            //5.如果该父节点没有child,初始化一个空的。
            if (parent.getChild() == null) {
                parent.setChild(new ArrayList<>());
            }
            //6.把循环到的节点,添加到父节点。
            parent.getChild().add(item);
        }
        return rootData;
    }

树过滤

使用递归的方式,对节点进行过滤。

过滤条件

/**
 * 树条件过滤
 *
 * @author dcx
 * @since 2024-03-05 09:47
 **/
@Data
public class TreeFilter {
    private String name;
    private Integer page;
    private Integer pageSize;
}

树过滤

/**
     * 树过滤
     * @param trees	 全部树
     * @param treeFilter	树过滤条件
     * @return void
     * @author dcx
     * @date 2024/3/5 09:52
     **/
    public void filterTree(List<Tree> trees, TreeFilter treeFilter) {
        bottomToUpRetain(trees, (tree) -> {
            //是否删除
            String name = treeFilter.getName();
            if (!StringUtils.isEmpty(name)) {
                boolean contains = tree.getName().contains(name);
                if (!contains){
                    return true;
                }
            }
            return false;
        });

    }


    /**
     * 传入一棵树,递归到子,从子到父过节点,只要存在符合条件的子节点,则保留从父到子的路径树,
     * 根据预留函数式接口,利用模板方法设计模式,过滤全量资产树
     * 因java8 collection只提供removeIf,无提供retainIf,故传参判断是否保留
     *
     * @param treeNodeModels   全量资产树
     * @param templateFunction 函数接口,封装模板方法设计模式,封装资产节点查询条件
     * @param isRemove         是否是要从树节点删除
     */
    private void filerAssetTree(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction, boolean isRemove) {
        treeNodeModels.removeIf(item -> {
            // 有子节点,进递归
            if (checkNotEmpty(item.getChild())) {
                filerAssetTree(item.getChild(), templateFunction, isRemove);
            }
            // 无子节点,判断当前节点是否符合条件,不符合删除
            if (checkEmpty(item.getChild())) {
                // 判断是否删除,为true的删除
                return isRemove == templateFunction.apply(item);
            }
            // 跳出递归,还存在符合条件的子节点,不删除
            return false;
        });
    }

    private boolean checkNotEmpty(List<Tree> list) {
        return list != null && !list.isEmpty();
    }

    private boolean checkEmpty(List<Tree> list) {
        return list == null || list.isEmpty();
    }

    /**
     * 自下而上,从子到父,过滤出符合条件的节点,保留完整树结构
     *
     * @param treeNodeModels   资产树
     * @param templateFunction 资产节点查询条件
     */
    private void bottomToUpRetain(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction) {
        filerAssetTree(treeNodeModels, templateFunction, false);
    }

    /**
     * 自下而上,从子到父,删除掉符合条件的节点,留下来的节点,保留完整的树结构,从子到父
     *
     * @param treeNodeModels   资产树
     * @param templateFunction 资产节点查询条件
     */
    private void bottomToUpRemove(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction) {
        filerAssetTree(treeNodeModels, templateFunction, true);
    }

    /**
     * 传入一棵树,由上到下过滤节点,过滤出符合条件的节点,只要不符合条件的节点,从树从删除该节点
     * 根据预留函数式接口,利用模板方法设计模式,过滤全量资产树
     * 因java8 collection只提供removeIf,无提供retainIf,故传参判断是否保留
     *
     * @param treeNodeModels   资产树
     * @param templateFunction 函数接口,封装模板方法设计模式,封装资产节点查询条件
     * @param isRemove         是否是要从树节点删除
     */
    private void filterTopToDown(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction, boolean isRemove) {
        treeNodeModels.removeIf(item -> {
            // 判断当前节点是否符合条件,为true的删除
            if (templateFunction.apply(item)) {
                return isRemove == templateFunction.apply(item);
            }
            // 如果当前节点符合条件,并且存在子节点,则递归进入子节点继续遍历
            if (!item.getChild().isEmpty()) {
                filterTopToDown(item.getChild(), templateFunction, isRemove);
            }
            // 子节点符合条件,则保留
            return false;
        });
    }

    /**
     * 自上而下,从父到子,过滤出符合条件的节点
     *
     * @param treeNodeModels   资产树
     * @param templateFunction 资产节点查询条件
     */
    private void topToDownRetain(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction) {
        filterTopToDown(treeNodeModels, templateFunction, false);
    }

    /**
     * 自上而下,从父到子,删除掉符合条件的节点
     *
     * @param treeNodeModels   资产树
     * @param templateFunction 资产节点查询条件
     */
    private void topToDownRemove(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction) {
        filterTopToDown(treeNodeModels, templateFunction, true);
    }

树分页

 /**
     * 树分页
     * @param trees	
     * @param treeFilter	
     * @return java.util.List<com.skuu.math.tree.page.Tree>
     * @author dcx
     * @date 2024/3/5 10:09
     **/
    public List<Tree> page(List<Tree> trees, TreeFilter treeFilter) {
        Integer pageSize = treeFilter.getPageSize();
        List<List<Tree>> partition = Lists.partition(trees, pageSize);
        Integer page = treeFilter.getPage();
        return partition.get(page);
    }

使用

public static void main(String[] args) {
        TreeUtil treeUtil = new TreeUtil();
        //列表
        ArrayList<Tree> trees = Lists.newArrayList();
        Long parentId = 1L;
        //列表转树
        List<Tree> treeTree = treeUtil.listToTree(trees, parentId);
        //过滤条件
        TreeFilter treeFilter = new TreeFilter();
        //树过滤
        treeUtil.filterTree(treeTree,treeFilter);
        //树分页
        treeUtil.page(treeTree,treeFilter);
    }

全代码

package com.skuu.math.tree.page;

import com.google.common.collect.Lists;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/**
 * @author dcx
 * @since 2024-03-04 17:54
 **/
public class TreeUtil {

    /**
     * 列表转树
     *
     * @param list     节点列表
     * @param parentId 根id
     * @return java.util.List<com.skuu.math.tree.page.Tree>
     * @author dcx
     * @date 2024/3/4 18:04
     **/
    public List<Tree> listToTree(List<Tree> list, Long parentId) {
        Map<Long, Tree> idMap = new HashMap<>();
        List<Tree> rootData = new ArrayList<>();
        for (Tree item : list) {
            //1.遍历找到顶层节点
            if (item.getPid().equals(parentId)) {
                rootData.add(item);
            }
            //2.空间换时间,将tree缓存起来
            idMap.put(item.getId(), item);
        }
        for (Tree item : list) {
            Long tmpParentId = item.getPid();
            // 3.排除根节点
            if (tmpParentId.equals(parentId)) {
                continue;
            }
            //4.没有根节点的,不显示。
            Tree parent = idMap.get(tmpParentId);
            if (parent == null) {
                continue;
            }
            //5.如果该父节点没有child,初始化一个空的。
            if (parent.getChild() == null) {
                parent.setChild(new ArrayList<>());
            }
            //6.把循环到的节点,添加到父节点。
            parent.getChild().add(item);
        }
        return rootData;
    }

    /**
     * 树过滤
     *
     * @param trees      全部树
     * @param treeFilter 树过滤条件
     * @return void
     * @author dcx
     * @date 2024/3/5 09:52
     **/
    public void filterTree(List<Tree> trees, TreeFilter treeFilter) {
        bottomToUpRetain(trees, (tree) -> {
            //是否删除
            String name = treeFilter.getName();
            if (!StringUtils.isEmpty(name)) {
                boolean contains = tree.getName().contains(name);
                if (!contains) {
                    return true;
                }
            }
            return false;
        });

    }


    /**
     * 传入一棵树,递归到子,从子到父过节点,只要存在符合条件的子节点,则保留从父到子的路径树,
     * 根据预留函数式接口,利用模板方法设计模式,过滤全量资产树
     * 因java8 collection只提供removeIf,无提供retainIf,故传参判断是否保留
     *
     * @param treeNodeModels   全量资产树
     * @param templateFunction 函数接口,封装模板方法设计模式,封装资产节点查询条件
     * @param isRemove         是否是要从树节点删除
     */
    private void filerAssetTree(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction, boolean isRemove) {
        treeNodeModels.removeIf(item -> {
            // 有子节点,进递归
            if (checkNotEmpty(item.getChild())) {
                filerAssetTree(item.getChild(), templateFunction, isRemove);
            }
            // 无子节点,判断当前节点是否符合条件,不符合删除
            if (checkEmpty(item.getChild())) {
                // 判断是否删除,为true的删除
                return isRemove == templateFunction.apply(item);
            }
            // 跳出递归,还存在符合条件的子节点,不删除
            return false;
        });
    }

    private boolean checkNotEmpty(List<Tree> list) {
        return list != null && !list.isEmpty();
    }

    private boolean checkEmpty(List<Tree> list) {
        return list == null || list.isEmpty();
    }

    /**
     * 自下而上,从子到父,过滤出符合条件的节点,保留完整树结构
     *
     * @param treeNodeModels   资产树
     * @param templateFunction 资产节点查询条件
     */
    private void bottomToUpRetain(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction) {
        filerAssetTree(treeNodeModels, templateFunction, false);
    }

    /**
     * 自下而上,从子到父,删除掉符合条件的节点,留下来的节点,保留完整的树结构,从子到父
     *
     * @param treeNodeModels   资产树
     * @param templateFunction 资产节点查询条件
     */
    private void bottomToUpRemove(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction) {
        filerAssetTree(treeNodeModels, templateFunction, true);
    }

    /**
     * 传入一棵树,由上到下过滤节点,过滤出符合条件的节点,只要不符合条件的节点,从树从删除该节点
     * 根据预留函数式接口,利用模板方法设计模式,过滤全量资产树
     * 因java8 collection只提供removeIf,无提供retainIf,故传参判断是否保留
     *
     * @param treeNodeModels   资产树
     * @param templateFunction 函数接口,封装模板方法设计模式,封装资产节点查询条件
     * @param isRemove         是否是要从树节点删除
     */
    private void filterTopToDown(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction, boolean isRemove) {
        treeNodeModels.removeIf(item -> {
            // 判断当前节点是否符合条件,为true的删除
            if (templateFunction.apply(item)) {
                return isRemove == templateFunction.apply(item);
            }
            // 如果当前节点符合条件,并且存在子节点,则递归进入子节点继续遍历
            if (!item.getChild().isEmpty()) {
                filterTopToDown(item.getChild(), templateFunction, isRemove);
            }
            // 子节点符合条件,则保留
            return false;
        });
    }

    /**
     * 自上而下,从父到子,过滤出符合条件的节点
     *
     * @param treeNodeModels   资产树
     * @param templateFunction 资产节点查询条件
     */
    private void topToDownRetain(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction) {
        filterTopToDown(treeNodeModels, templateFunction, false);
    }

    /**
     * 自上而下,从父到子,删除掉符合条件的节点
     *
     * @param treeNodeModels   资产树
     * @param templateFunction 资产节点查询条件
     */
    private void topToDownRemove(List<Tree> treeNodeModels, Function<Tree, Boolean> templateFunction) {
        filterTopToDown(treeNodeModels, templateFunction, true);
    }

    /**
     * 树分页
     * @param trees
     * @param treeFilter
     * @return java.util.List<com.skuu.math.tree.page.Tree>
     * @author dcx
     * @date 2024/3/5 10:09
     **/
    public List<Tree> page(List<Tree> trees, TreeFilter treeFilter) {
        Integer pageSize = treeFilter.getPageSize();
        List<List<Tree>> partition = Lists.partition(trees, pageSize);
        Integer page = treeFilter.getPage();
        return partition.get(page);
    }

    public static void main(String[] args) {
        TreeUtil treeUtil = new TreeUtil();
        //列表
        ArrayList<Tree> trees = Lists.newArrayList();
        Long parentId = 1L;
        //列表转树
        List<Tree> treeTree = treeUtil.listToTree(trees, parentId);
        //过滤条件
        TreeFilter treeFilter = new TreeFilter();
        //树过滤
        treeUtil.filterTree(treeTree,treeFilter);
        //树分页
        treeUtil.page(treeTree,treeFilter);
    }

}

Last updated

Was this helpful?