数据结构与表结构
树表是一种数据结构,用于展示具有层级关系的数据。
在数据库中通常以邻接表法存储(存储父节点)。
![](https://i-blog.csdnimg.cn/blog_migrate/93f48a0452604b53684056eaae61967e.png)
问题
前端往往是需要一个树形结构。
后端应该如何生成这样一棵树呢?
如何保证修改操作不会出现环呢?
删除操作如何判断是否存在子节点?
最后如何保证操作的效率?
实体类
由于使用了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;
}
增加部门
增加部门并维护其父节点。因为containsKey
是LinkedHashSet
中的方法,所以复杂度为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);
}
}
修改
修改名字没什么好讲,主要是修改上级部门。
修改上级分两步:
- 判断新的父部门是否是自己,或者自己的子部门。(防止产生环)
- 修改数据库以及维护缓存。这里修改缓存需要先从原先自父节点中移除自己。再将自己添加到新的父节点中。
其中第一步复杂度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)