部门树的高效解决方案

数据结构与表结构

树表是一种数据结构,用于展示具有层级关系的数据。

在数据库中通常以邻接表法存储(存储父节点)。

数据库数据

问题

        前端往往是需要一个树形结构。

        后端应该如何生成这样一棵树呢?

        如何保证修改操作不会出现环呢?

        删除操作如何判断是否存在子节点?

        最后如何保证操作的效率?

实体类

        由于使用了hashSet和hashMap,所以部门实体类和树都需要重写equals和hashCode方法。

数据库对应的Dept

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept implements Cloneable {
    String id; // 部门ID
    String name; // 部门名称
    String pid; // 父部门ID
    @Override
    public Dept clone() {
        try {
            return (Dept) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Dept dept = (Dept) o;
        return Objects.equals(id, dept.id);
    }
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

返回给前端的Tree

@Data
@AllArgsConstructor
@NoArgsConstructor
/**
 * 树节点类
 * @param <T> 节点值的类型
 */
public class Tree<T> {
    T val; // 节点
    Set<Tree<T>> children=new LinkedHashSet<>(); // 子节点集合

    /**
     * 构造方法,创建一个树节点
     * @param val 节点值
     */
    public Tree(T val) {
        this.val = val;
    }

    /**
     * 移除指定的子节点
     * @param tree 要移除的子节点
     * @return 是否移除成功
     */
    public boolean remove(Tree<T> tree){
        return children.remove(tree);
    }

    /**
     * 添加指定的子节点
     * @param tree 要添加的子节点
     * @return 是否添加成功
     */
    public boolean add(Tree<T> tree){
        return children.add(tree);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Tree<?> tree = (Tree<?>) o;
        return Objects.equals(val, tree.val);
    }

    @Override
    public int hashCode() {
        return Objects.hash(val);
    }
}

操作

        因为获取部门是一个极其频繁的操作,所以我对它进行了一个缓存(下面的treeMap),并对它进行维护。这样可以使得每次查询部门树都是O(1)的时间复杂度。

构建树并存入缓存

先将所有节点放入Map中,在将所有节点添加自己的父节点中。时间复杂度O(N)

    DeptDao deptDao = new DeptDao();  // DeptDao对象,这里是模拟dao
    Map<String, Tree<Dept>> treeMap=new HashMap<>();  // 缓存部门树
    /**
     * 构建部门树形结构,时间复杂度O(N),空间复杂度O(N)
     * 先将所有节点放入Map中,在将所有节点添加自己的父节点中。时间复杂度O(N)
     */
    private void builder() {
        List<Dept> list = deptDao.list();  // 获取部门列表
        treeMap.put("0", new Tree<>(new Dept("0", null, "-1")));    // 根节点。如果不想返回根节点,可以在查询时返回根节点的子节点
        //1.放入map集合中
        list.forEach(dept -> treeMap.put(dept.getId(), new Tree<Dept>(dept)));  // 将部门对象添加到TreeMap中

        // 将部门对象添加到父节点中
        list.forEach(o -> {
            Tree<Dept> tree = treeMap.get(o.getPid());
            if (tree == null) return;
            tree.getChildren().add(treeMap.get(o.getId()));
        });
    }

查询某个部门下的部门树

由于我们构建树时将树缓存起来了,并且我们获取的就是一颗完整的树。时间复杂度O(1)

	/**
     * 获取指定id的部门树形结构
     * @param id 部门id
     * @return 部门树形结构
     */
    public Tree<Dept> getTree(String id) {
        return treeMap.get(id);
    }

查询某部门下的直接子部门

我们缓存的是树结构,不能直接返回,需要做一个映射。时间复杂度O(m)(m为直接子节点的个数)

    /**
     * 根据指定的id返回部门树的子部门列表
     *
     * @param id 部门id
     * @return 子部门列表
     */
    public List<Dept> list(String id) {
        // 获取指定id对应的部门树
        Tree<Dept> deptTree = treeMap.get(id);
        // 获取部门树的子部门列表。
        List<Dept> children = deptTree.getChildren().stream().map(tree -> tree.getVal()).collect(Collectors.toList());
        // 返回子部门列表
        return children;
    }

增加部门

增加部门并维护其父节点。因为containsKeyLinkedHashSet中的方法,所以复杂度为O(1)

    /**
     * 添加部门
     * @param dept 部门对象。请在控制层做不为null校验
     * @return 添加成功返回true,否则返回false
     */
    public boolean add(Dept dept) {
        // 判断是否已存在
        if (treeMap.containsKey(dept.getId())) {
            throw new RuntimeException("部门已存在");
        }
        // 判断上级部门是否存在。注:如要添加一级部门,请将pid设为0。
        if (!treeMap.containsKey(dept.getPid())) {
            throw new RuntimeException("父部门不存在");
        }
        // 添加部门
        if (deptDao.add(dept)) {
            // 添加到父节点中
            treeMap.get(dept.getPid()).add(new Tree<Dept>(dept));
            return true;
        }
        return false;
    }

删除部门

在删除部门之前,需要先判断一下该部门下是否有子部门。再进行删除。复杂度O(1)

判断是否存在子部门
    /**
     * 判断指定id的部门是否存在子部门
     * @param id 部门id
     * @return 是否存在子部门
     */
    public boolean hasChild(String id) {
        return treeMap.get(id).getChildren().size() > 0;
    }
删除
    /**
     * 删除指定id的部门
     * @param id 部门id
     * @return 是否成功删除部门
     */
    public void delete(String id) {
        if (hasChild(id)) {
            throw new RuntimeException("该部门存在子部门,不能删除");
        }
        if (deptDao.remove(id)) {
            treeMap.remove(id);
        }
    }

修改

修改名字没什么好讲,主要是修改上级部门。

修改上级分两步:

  1. 判断新的父部门是否是自己,或者自己的子部门。(防止产生环)
  2. 修改数据库以及维护缓存。这里修改缓存需要先从原先自父节点中移除自己。再将自己添加到新的父节点中。

其中第一步复杂度O(k)(新父节点到更节点的长度),第二步时间复杂度O(1)。

判断是否为环
    /**
     * 判断是否存在循环部门
     *
     * @param dept 要更新的部门对象
     * @return 是否存在循环部门
     */
    private boolean isLoop(Dept dept) {
        // 当前部门id
        String id = dept.getId();
        Tree<Dept> parent = treeMap.get(dept.getPid()); //获取父节点
        while (parent != null) {
            Dept p = parent.getVal();
            // 如果当前部门id和父级id相等,则存在循环部门,返回true
            if (id.equals(p.getId())) {
                return true;
            }
            //向上级遍历
            parent = treeMap.get(p.getPid());
        }
        // 不存在循环部门,返回false
        return false;
    }
更新部门信息
    /**
     * 更新部门信息
     *
     * @param newDept 待更新的部门对象。Dept在控制层需要做数据校验,如id不为空,pid不为空。id!=0。
     */
    public void update(Dept newDept) {
        // 判断是否为循环部门
        if (isLoop(newDept)) {
            throw new RuntimeException("不能设置父部门为自己或子部门");
        }

        // 获取要更新的部门节点
        Tree<Dept> treeNode = treeMap.get(newDept.getId());
        // 获取原部门节点
        Dept oldNode = treeNode.getVal();

        // 更新部门信息
        if (deptDao.update(newDept)) {
            // 从原父节点中删除部门节点
            treeMap.get(oldNode.getPid()).remove(treeNode);
            // 添加到新父节点中
            treeMap.get(newDept.getPid()).add(treeNode);
        }
    }

完整的增删改查代码

public class DeptService {
    DeptDao deptDao = new DeptDao();  // DeptDao对象,这里是模拟dao
    Map<String, Tree<Dept>> treeMap = new HashMap<>();  // 存储部门树

    public DeptService() {
        builder();  // 构建部门树
    }

    /**
     * 构建部门树形结构,时间复杂度O(N),空间复杂度O(N)
     * 先将所有节点放入Map中,在将所有节点添加自己的父节点中。时间复杂度O(N)
     */
    private void builder() {
        List<Dept> list = deptDao.list();  // 获取部门列表
        treeMap.put("0", new Tree<>(new Dept("0", null, "-1")));    // 根节点。如果不想返回根节点,可以在查询时返回根节点的子节点
        //1.放入map集合中
        list.forEach(dept -> treeMap.put(dept.getId(), new Tree<Dept>(dept)));  // 将部门对象添加到TreeMap中

        // 将部门对象添加到父节点中
        list.forEach(o -> {
            Tree<Dept> tree = treeMap.get(o.getPid());
            if (tree == null) return;
            tree.getChildren().add(treeMap.get(o.getId()));
        });
    }

    /**
     * 获取指定id的部门树形结构
     *
     * @param id 部门id
     * @return 部门树形结构
     */
    public Tree<Dept> getTree(String id) {
        return treeMap.get(id);
    }

    /**
     * 根据指定的id返回部门树的子部门列表
     *
     * @param id 部门id
     * @return 子部门列表
     */
    public List<Dept> list(String id) {
        // 获取指定id对应的部门树
        Tree<Dept> deptTree = treeMap.get(id);
        // 获取部门树的子部门列表。
        List<Dept> children = deptTree.getChildren().stream().map(tree -> tree.getVal()).collect(Collectors.toList());
        // 返回子部门列表
        return children;
    }

    /**
     * 添加部门
     *
     * @param dept 部门对象。请在控制层做不为null校验
     * @return 添加成功返回true,否则返回false
     */
    public boolean add(Dept dept) {
        // 判断是否已存在
        if (treeMap.containsKey(dept.getId())) {
            throw new RuntimeException("部门已存在");
        }
        // 判断上级部门是否存在。注:如要添加一级部门,请将pid设为0。
        if (!treeMap.containsKey(dept.getPid())) {
            throw new RuntimeException("父部门不存在");
        }
        // 添加部门
        if (deptDao.add(dept)) {
            // 添加到父节点中
            treeMap.get(dept.getPid()).add(new Tree<Dept>(dept));
            return true;
        }
        return false;
    }


    /**
     * 更新部门信息
     *
     * @param newDept 待更新的部门对象。Dept在控制层需要做数据校验,如id不为空,pid不为空。id!=0。
     */
    public void update(Dept newDept) {
        // 判断是否为循环部门
        if (isLoop(newDept)) {
            throw new RuntimeException("不能设置父部门为自己或子部门");
        }

        // 获取要更新的部门节点
        Tree<Dept> treeNode = treeMap.get(newDept.getId());
        // 获取原部门节点
        Dept oldNode = treeNode.getVal();

        // 更新部门信息
        if (deptDao.update(newDept)) {
            // 从原父节点中删除部门节点
            treeMap.get(oldNode.getPid()).remove(treeNode);
            // 添加到新父节点中
            treeMap.get(newDept.getPid()).add(treeNode);
        }
    }

    /**
     * 判断是否存在循环部门
     *
     * @param dept 要更新的部门对象
     * @return 是否存在循环部门
     */
    private boolean isLoop(Dept dept) {
        // 当前部门id
        String id = dept.getId();
        Tree<Dept> parent = treeMap.get(dept.getPid()); //获取父节点
        while (parent != null) {
            Dept p = parent.getVal();
            // 如果当前部门id和父级id相等,则存在循环部门,返回true
            if (id.equals(p.getId())) {
                return true;
            }
            //向上级遍历
            parent = treeMap.get(p.getPid());
        }
        // 不存在循环部门,返回false
        return false;
    }

    /**
     * 判断指定id的部门是否存在子部门
     *
     * @param id 部门id
     * @return 是否存在子部门
     */
    public boolean hasChild(String id) {
        return !treeMap.get(id).getChildren().isEmpty();
    }

    /**
     * 删除指定id的部门
     *
     * @param id 部门id:需要在控制层做数据校验,例如不为空,id!=0
     * @return 是否成功删除部门
     */
    public void delete(String id) {
        if (hasChild(id)) {
            throw new RuntimeException("该部门存在子部门,不能删除");
        }
        if (deptDao.remove(id)) {
            treeMap.remove(id);
        }
    }
}

最坏情况时间复杂度

该方案

  • 新增:O(1)
  • 查询树:O(1)
  • 修改:O(n)
  • 删除:O(1)
  • 查询直接子部门:O(n)

空间复杂度均为O(n)

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值