此文將介紹一種簡單可行的多級樹結構算法,並支持節點的上下移動。算法
首先,本文的算法是啓蒙於一個.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"
}
]
}