此文将介绍一种简单可行的多级树结构算法,并支持节点的上下移动。算法
首先,本文的算法是启蒙于一个.net项目中的多级树结构算法。该项目中,全部节点的排序值,统统按照显示顺序排列(如图)。数据库
这种方式的缺点是:当“插入”,“移动”,“修改(修改所属父节点)”和“删除”节点,须要对子节点和父节点的排序值都要从新设置(须要有序下移当前节点以后的全部节点。若是只是一个简单的二叉树,那还好,但若是是多级树,则使得逻辑异常复杂)。json
那么,如何在此基础之上,进行修改。获得一个简单的算法,在插入或移动节点时,不用使用那么复杂的算法呢?答案以下:app
1.(表)结构ide
表结构不变(Id,ParentId,Position,Name ... ),排序值的设置逻辑变成:局部排序,即,只作(same level)同级别的节点的排序。(以下图)this
2.节移动,节点移动规则,只作同级节点之间上下移动(采用新结构以后,也就屏蔽掉了移动一个节点时,还得同时移动当前节点以后全部节点的排序位的烦恼,如今就只要关系本身的排序位就好了。),非同级移动能够经过修改挂靠父节点来作到。编码
3.新增,只用查询同级节点中Max 排序位,而后+1做为本身的排序位便可。spa
4.修改,保持当前排序位不变。若是是修改挂靠父节点,也只是单纯修改ParentId便可,当前节点排序修改成未来父节点中子节点中Max排序值,当前节点的子节点的排序值不用变。.net
5.删除,将当前节点以后的同级节点的排序值+1,其余不变。对象
最后,JAVA CODE直接上代码!
因为管理系统中,有不少地方都用到树形结构,因此抽象继承是必须的。
第一大类就是TreeRsVo,全部树形结构的model类都继承它,TreeRsVo使用泛型来指定“子节点列表”属性(由于每一个树形类可能有本身特有的属性,而“子节点列表”的元素类型就是自身,因此经过泛型进行传递,复用TreeRsVo基类里面的排序方法)。
/**
* 树-基类
* Created by TonyZeng on 2017/4/21.
*/
public class TreeRsVo<T extends TreeRsVo> {
private Long id;//ID
private Long parentId;//父节点ID
private String name;//名
private Boolean available;//是否可用
private Integer position;//位置(用于排序)
private List<T> children;//子列表
//此处省略部分get&set方法
/**
* 孩子节点排序
*/
public void sortChildren() {
this.children.sort((m1, m2) -> {
int orderBy1 = m1.getPosition();
int orderBy2 = m2.getPosition();
return orderBy1 < orderBy2 ? -1 : (orderBy1 == orderBy2 ? 0 : 1);
});
// 对每一个节点的下一层节点进行排序
for (T n : children) {
if (n != null && n.getChildren() != null) {
n.sortChildren();
}
}
}
}
/**
* 系统菜单-响应Vo(树)(此类Mapping from DB Table Model Class,即至关于数据库model类)
* Created by TonyZeng on 2017/3/13.
*/
public class MenuResponseVo extends TreeRsVo<MenuResponseVo> {
private String link; //连接
private String icon;//图标
private List<ButtonResponseVo> buttons;//按钮列表
//此处省略部分get&set方法
}
/**
* 建立有序树
* Created by TonyZeng on 2016/5/13.
*/
public class TreeMapper {
/**
* 建立有序菜单树
* @param list
* @return
*/
public static MenuResponseVo MenuTree(List<MenuResponseVo> list) {
//组装Map数据,将全部菜单所有放到Map中,并用菜单ID做为Key来标记,方便后面的各节点寻找父节点。
Map<Long, MenuResponseVo> dataMap = new HashMap<>();//<{菜单ID},{菜单对象}>
for (MenuResponseVo menu : list) {
dataMap.put(menu.getId(), menu);
}
//建立根节点
MenuResponseVo root = new MenuResponseVo();
//组装树形结构,将子节点所有挂靠到本身的父节点
for (Map.Entry<Long, MenuResponseVo> entry : dataMap.entrySet()) {
MenuResponseVo menu = entry.getValue();
if (menu.getParentId().equals(0L)) {
root.getChildren().add(menu);
} else {
dataMap.get(menu.getParentId()).getChildren().add(menu);
}
}
//对多级树形结构进行“二叉树排序”
root.sortChildren();
return root;
}
}
/**
* 系统设置-系统菜单设置服务
* Created by TonyZeng on 2017/2/6.
*/
@Service("sysMenuBIService")
public class SysMenuBIServiceImpl implements SysMenuBIService {
@Autowired
SysMenuService sysMenuService;
/**
* 获取菜单树形结构接口
*
* @return 完整的菜单多级树形结构
*/
@Override
public BaseDto getMenuList() {
List<MenuResponseVo> list = new ArrayList<>();
for (SysMenu x : sysMenuService.findAll(new Sort(Sort.Direction.ASC, "Position"))) {
list.add(VoMapper.mapping(x));
}
return new BaseDto(TreeMapper.MenuTree(list).getChildren());
}
/**
* 添加菜单
*
* @param requestVo
* @return
*/
@Override
public BaseDto addMenu(AddMenuRqVo requestVo) {
//验证参数
String validateResult = new ValidateUtil<AddMenuRqVo>().validate(requestVo);
if (validateResult != null) {
return new BaseDto(-1, validateResult);
}
try {
//新增时,检查是否存在相同的菜单编码
List sameCodeMenus = sysMenuService.findByMenuCode(requestVo.getCode());
if (sameCodeMenus != null && sameCodeMenus.size() > 0) {
return new BaseDto(-1, "菜单编码已存在,请从新填写");
}
//vo to po
SysMenu model = PoMapper.mapping(requestVo);
//查询兄弟节点中,最大的排序值,并加一做为本身的排序值
model.setPosition(sysMenuService.findMaxPositionOfBrother(requestVo.getParentId()) + 1);
model = sysMenuService.save(model);
if (model.getId() != null) {
return new BaseDto(model.getId());
} else {
return new BaseDto(-1, "系统设置-添加菜单 失败");
}
} catch (Exception ex) {
return new BaseDto(-1, "系统设置-添加菜单 失败");
}
}
/**
* 更新菜单
*
* @param requestVo
* @return
*/
@Override
public BaseDto updateMenu(UpdateMenuRqVo requestVo) {
//验证参数
String validateResult = new ValidateUtil<AddMenuRqVo>().validate(requestVo);
if (validateResult != null) {
return new BaseDto(-1, validateResult);
}
try {
//新增时,检查是否存在相同的菜单编码
SysMenu po = sysMenuService.find(requestVo.getId());
if (po == null) {
return new BaseDto(-1, "菜单不存在");
}
SysMenu model = PoMapper.mapping(requestVo);
//排序值不变
model.setPosition(po.getPosition());
model = sysMenuService.save(model);
if (model.getId() != null) {
return new BaseDto(model.getId());
} else {
return new BaseDto(-1, "系统设置-更新菜单 失败");
}
} catch (Exception ex) {
return new BaseDto(-1, "系统设置-更新菜单 失败");
}
}
/**
* 删除菜单
*
* @param id 菜单id (修改菜单必填)
* @return
*/
@Override
public BaseDto deleteMenu(Long id) {
//验证参数
if (id == null) {
return new BaseDto(-1, "请提供id");
}
try {
SysMenu po = sysMenuService.find(id);
if (po == null) {
return new BaseDto(-1, "菜单不存在");
}
//删除
sysMenuService.delete(id);
//重置菜单的排序值
resetMenu(po.getParentId());
return new BaseDto(id);
} catch (Exception ex) {
return new BaseDto(-1, "系统设置-删除菜单 失败");
}
}
/**
* 移动菜单排序
* 算法:
* 1.找到(previous or next)菜单
* 2.与其交换位置
*
* @param requestVo
* @return
*/
@Override
public BaseDto moveMenu(MoveRqVo requestVo) {
//验证参数
String validateResult = new ValidateUtil<MoveRqVo>().validate(requestVo);
if (validateResult != null) {
return new BaseDto(-1, validateResult);
}
try {
//新增时,检查是否存在相同的菜单编码
SysMenu self = sysMenuService.find(requestVo.getId());
if (self == null) {
return new BaseDto(-1, "菜单不存在");
}
List<SysMenu> brotherList = sysMenuService.findByParentId(self.getParentId());
//若是有兄弟节点
if (brotherList.size() > 0) {
//Get index of this menus in brother menus.
int indexOfThisMenus = 0;
for (int i = 0; i < brotherList.size(); i++) {
if (brotherList.get(i).getId().equals(self.getId())) {
indexOfThisMenus = i;
}
}
SysMenu brother;
if (requestVo.getDown().equals(true)) {
//判断是否已经为最底部的菜单
if (indexOfThisMenus == (brotherList.size() - 1)) {
return new BaseDto(-1, "已是(同级中)最底部的菜单了");
} else {
//获取后临的兄弟菜单
brother = brotherList.get(indexOfThisMenus + 1);
}
//交换位置
brother.setPosition(brother.getPosition() - 1);
self.setPosition(self.getPosition() + 1);
} else {
//判断是否已经为最底部的菜单
if (indexOfThisMenus == 0) {
return new BaseDto(-1, "已是(同级中)最顶部的菜单了");
} else {
//获取前临的兄弟菜单
brother = brotherList.get(indexOfThisMenus - 1);
}
//交换位置
brother.setPosition(brother.getPosition() + 1);
self.setPosition(self.getPosition() - 1);
}
sysMenuService.save(brother);
sysMenuService.save(self);
if (self.getId() != null && brother.getId() != null) {
return new BaseDto(self.getId());
} else {
return new BaseDto(-1, "系统设置-移菜单位置 失败");
}
} else {
//若是没有兄弟节点
return new BaseDto(self.getId());
}
} catch (Exception ex) {
return new BaseDto(-1, "系统设置-移菜单位置 失败");
}
}
/**
* 重置菜单的排序值
*
* @param parentId 父节点ID
* @return
*/
@Override
public BaseDto resetMenu(Long parentId) {
return resetMenu(sysMenuService.findByParentId(parentId));
}
/**
* 重置菜单的排序值
*
* @param brothers 兄弟节点列表
* @return
*/
private BaseDto resetMenu(List<SysMenu> brothers) { //若是有兄弟节点
try {
if (brothers.size() > 0) {
for (int i = 0; i < brothers.size(); i++) {
brothers.get(i).setPosition(i + 1);
}
sysMenuService.save(brothers);
return new BaseDto(0, "系统设置-重置菜单的排序值 成功");
} else {
return new BaseDto(0, "系统设置-重置菜单的排序值 失败");
}
} catch (Exception ex) {
return new BaseDto(-1, "系统设置-重置菜单的排序值 失败");
}
}
}
{
"data": [
{
"id": 1,
"parentId": 0,
"name": "会员档案",
"available": true,
"position": 1,
"children": [
{
"id": 33,
"parentId": 1,
"name": "档案管理",
"available": true,
"position": 1,
"children": [],
"link": "~/user/CustomerMgr.aspx",
"icon": "user"
},
{
"id": 29,
"parentId": 1,
"name": "密码修改",
"available": true,
"position": 2,
"children": [
{
"id": 30,
"parentId": 29,
"name": "密码修改Child44",
"available": true,
"position": 1,
"children": [],
"link": "",
"icon": ""
},
{
"id": 26,
"parentId": 29,
"name": "密码修改Child1",
"available": true,
"position": 2,
"children": [],
"link": "",
"icon": ""
}
],
"link": "~/user/PersonalInfoMgr.aspx",
"icon": "userhome"
}
],
"link": "",
"icon": "groupgear"
}
]
}