1、前言
? 樹結構是一種較為常見的資料結構,如功能權限樹、企業的組織結構圖、行政區劃結構圖、家族譜、信令訊息樹等,都表現為樹型資料結構,
? 樹結構資料的共性是樹節點之間都有相互關系,對于一個節點物件,可以找到父節點、左鄰節點、右鄰節點、子節點串列,節點本身有其資料型別物件,不同型別的樹,不同之處在于節點資料型別不同,
? 下面針對樹型資料,用Java語言給出一種通用樹結構資料定義,并提供常規的樹節點操作方法,
2、樹節點類定義
2.1、基本概念
- 樹節點:即treeNode,樹節點是樹結構的基本元素,整棵樹是由一些列樹節點串接而成,每個樹節點,其父節點、左鄰節點、右鄰節點,或者是唯一的,或者為空,(否則成網狀結構了),樹節點還包含子節點串列及自身資料物件,
- 根節點:即rootNode,一棵樹的根節點是唯一的,根節點的父節點為空,常見的樹型結構資料,看起來好像有一組根節點,如導航欄選單、選單欄,實際上那是根節點的一級子節點,為了便于資料庫對樹型資料的存盤,根節點的節點ID規定為0,
- 葉子節點:即leafNode,葉子節點為樹的末梢,葉子節點不包含子節點,
- 樹:使用樹節點物件來表示一棵樹,由于樹節點包含子節點,子節點又包含子子節點,因此一個樹節點,就是一棵子樹;如果樹節點為根節點,則表示整棵樹,
- 父節點:樹節點的父節點,當前樹節點在父節點的子節點串列中,
- 子節點:樹節點的子節點,在當前節點的子節點串列中,
- 上級節點:或稱祖先節點,從根節點到當前節點的路徑上,不含當前節點的所有節點,都是上級節點,
- 下級節點:或稱子孫節點,以當前節點為上級節點的所有節點,都是下級節點,
- 左鄰節點:或稱左兄弟節點,或前一節點,與當前節點擁有相同的父節點,且在父節點的子節點串列中,在當前節點的前一個,
- 右鄰節點:或稱右兄弟節點,或后一節點,與當前節點擁有相同的父節點,且在父節點的子節點串列中,在當前節點的后一個,
- 節點資料:每個節點包含一個節點資料物件,不同種類的樹節點,其差異就是節點資料物件型別的不同,
2.2、樹節點類
? 樹節點類TreeNode,其定義如下:
package com.abc.questInvest.vo;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* @className : TreeNode
* @description : 樹節點
* @summary : 節點資料型別,必須實作ITreeNodeData介面類的介面
*
*/
@Data
public class TreeNode<T extends ITreeNodeData> implements Serializable {
private static final long serialVersionUID = 1L;
//節點資料物件
private T nodeData;
//父節點物件
private TreeNode<T> parent;
//子節點串列
private List<TreeNode<T>> children = new ArrayList<TreeNode<T>>();
//是否包含在樹中,1表示包含,0表示不包含,此屬性為附加屬性,在完整樹剪枝時使用
private Integer isIncluded = 0;
}
? 樹節點類TreeNode使用泛型T,來表示節點資料型別,規定T必需實作ITreeNodeData介面類,使用介面類而不是基類,是為了不限定節點資料的欄位集,且沒有多重繼承的問題,另外TreeNode也需要實作Serializable介面類,提供節點資料的序列化方法,
? TreeNode類提供下列屬性欄位:
- nodeData欄位,節點資料物件,資料型別為泛型T,使用泛型,來達到通用樹節點的目的,
- parent欄位,父節點物件,型別仍然是TreeNode
,如果父節點為null,表示這是根節點, - children欄位,子節點串列,其成員仍是TreeNode
型別, - isIncluded欄位,當前節點是否包含在樹中,有時候,即使某個節點在樹中,通過此屬性欄位,仍然可以指示該節點是否需要被剪枝,
? TreeNode類,提供了父節點物件和子節點串列屬性欄位,從而具有向上搜索和向下搜索的能力,
? TreeNode類,沒有使用左鄰節點、右鄰節點屬性欄位,是考慮到兄弟節點的搜索不是很頻繁,不用左鄰節點、右鄰節點屬性欄位,可以減少節點關系維護的復雜度,同級節點搜索,可以遍歷父節點的子節點串列來實作,
3、樹節點資料介面類
樹節點資料介面類,為ITreeNodeData,其規定了樹節點資料物件型別,必需實作的介面方法,代碼如下:
package com.abc.questInvest.vo;
/**
* @className : ITreeNodeData
* @description : 樹節點資料介面類
*
*/
public interface ITreeNodeData extends Cloneable{
//=============節點基本屬性訪問介面==============================
//獲取節點ID
int getNodeId();
//獲取節點名稱
String getNodeName();
//獲取父節點ID
int getParentId();
//=============Cloneable類介面===================================
//克隆
public Object clone();
}
? ITreeNodeData類,繼承Cloneable,要求樹節點資料物件型別必需實作克隆(clone)介面方法,目的是實作物件復制,
? ITreeNodeData類定義了下列基本的介面方法:
- getNodeId方法,獲取節點ID,
- getNodeName方法,獲取節點名稱,
- getParentId方法,獲取父節點ID,
- clone方法,實作Cloneable介面類需要多載的方法,
4、完整的樹節點類
? 對樹節點類TreeNode,進行完善,提供必要的介面,代碼如下:
package com.abc.questInvest.vo;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* @className : TreeNode
* @description : 樹節點
* @summary : 節點資料型別,必須實作ITreeNodeData介面類的介面
*
*/
@Data
public class TreeNode<T extends ITreeNodeData> implements Serializable {
private static final long serialVersionUID = 1L;
//節點資料
private T nodeData;
//父節點物件
private TreeNode<T> parent;
//子節點
private List<TreeNode<T>> children = new ArrayList<TreeNode<T>>();
//是否包含在樹中,1表示包含,0表示不包含,此屬性為附加屬性,在完整樹剪枝時使用
private Integer isIncluded = 0;
/**
*
* @methodName : addChildNode
* @description : 添加子節點
* @param childNode : 子節點
*
*/
public void addChildNode(TreeNode<T> childNode) {
childNode.setParent(this);
children.add(childNode);
}
/**
*
* @methodName : removeChildNode
* @description : 移除子節點,如果子節點在子節點串列中,則移除,否則無影響
* @param childNode : 子節點
*
*/
public void removeChildNode(TreeNode<T> childNode) {
children.remove(childNode);
}
/**
*
* @methodName : getPrevSibling
* @description : 取得左鄰節點
* @return : 如果當前節點為第一個節點,則回傳null,否則為前一個節點
*
*/
public TreeNode<T> getPrevSibling(){
if (parent == null) {
//如果為根節點,則回傳null
return null;
}
List<TreeNode<T>> siblingList = parent.getChildren();
TreeNode<T> node = null;
for (int i = 0; i < siblingList.size(); i++) {
TreeNode<T> item = siblingList.get(i);
if (item == this) {
//找到當前節點
if (i > 0) {
//當前節點不是第一個子節點
//取得前一個節點
node = siblingList.get(i-1);
}
break;
}
}
return node;
}
/**
*
* @methodName : getNextSibling
* @description : 取得右鄰節點
* @return : 如果當前節點為最后一個節點,則回傳null,否則為后一個節點
*
*/
public TreeNode<T> getNextSibling(){
if (parent == null) {
//如果為根節點,則回傳null
return null;
}
List<TreeNode<T>> siblingList = parent.getChildren();
TreeNode<T> node = null;
for (int i = 0; i < siblingList.size(); i++) {
TreeNode<T> item = siblingList.get(i);
if (item == this) {
//找到當前節點
if (i < siblingList.size()-1) {
//當前節點不是最后一個子節點
//取得后一個節點
node = siblingList.get(i+1);
}
break;
}
}
return node;
}
/**
*
* @methodName : lookUpSubNode
* @description : 在當前節點及下級節點中查找指定節點ID的節點
* @param nodeId : 節點ID
* @return : 如果找到,回傳對應樹節點,否則回傳null
*
*/
public TreeNode<T> lookUpSubNode(int nodeId){
TreeNode<T> node = null;
//檢查當前節點
if (nodeData.getNodeId() == nodeId) {
node = this;
return node;
}
//遍歷子節點
for(TreeNode<T> item : children) {
node = item.lookUpSubNode(nodeId);
if (node != null) {
//如果節點非空,表示查找到了
break;
}
}
return node;
}
/**
*
* @methodName : lookUpSuperNode
* @description : 在當前節點及上級節點中查找指定節點ID的節點
* @param nodeId : 節點ID
* @return : 如果找到,回傳對應樹節點,否則回傳null
*
*/
public TreeNode<T> lookUpSuperNode(int nodeId){
TreeNode<T> node = null;
//檢查當前節點
if (nodeData.getNodeId() == nodeId) {
node = this;
return node;
}
//查找父節點
if (parent != null) {
node = parent.lookUpSuperNode(nodeId);
}
return node;
}
/**
*
* @methodName : clone
* @description : 復制樹節點,包括所有子節點
* @return : 復制后的樹節點
*
*/
@SuppressWarnings("unchecked")
public TreeNode<T> clone(){
//復制當前節點資料資訊
TreeNode<T> treeNode = new TreeNode<T>();
//節點資料
treeNode.setNodeData((T)this.nodeData.clone());
//是否包含
treeNode.setIsIncluded(this.isIncluded);
//復制所有子節點
for(TreeNode<T> item : this.children) {
//復制子節點
TreeNode<T> childNode = item.clone();
//加入子節點串列中
treeNode.addChildNode(childNode);
}
return treeNode;
}
/**
*
* @methodName : setChildrenIsIncluded
* @description : 設定所有子節點的是否包含屬性
* @param isIncluded : 節點是否包含
*
*/
public void setChildrenIsIncluded(Integer isIncluded) {
//遍歷所有子節點
for(TreeNode<T> item : this.children) {
item.setIsIncluded(isIncluded);
//子節點的子節點
for(TreeNode<T> subItem : item.getChildren()) {
subItem.setChildrenIsIncluded(isIncluded);
}
}
}
/**
*
* @methodName : combineTreeNode
* @description : 將結構完全相同的節點合并到本節點中,合并后的節點的isIncluded屬性位|操作
* @param combineNode: 并入的節點
*
*/
public void combineTreeNode(TreeNode<T> combineNode) {
//當前節點資料的isIncluded屬性,使用位|操作
this.setIsIncluded(this.getIsIncluded() | combineNode.getIsIncluded());
//合并子節點
for (int i = 0; i < children.size(); i++) {
TreeNode<T> item = children.get(i);
TreeNode<T> combineItem = combineNode.getChildren().get(i);
//合并子節點
item.combineTreeNode(combineItem);
}
}
/**
*
* @methodName : arrange
* @description : 對樹進行剪枝處理,即所有isIncluded為0的節點,都被移除
*
*/
public void arrange() {
//遍歷子節點串列,如果子節點的isIncluded為0,則剪枝
//倒序遍歷
for (int i = children.size() -1; i >=0; i--) {
TreeNode<T> item = children.get(i);
if (item.getIsIncluded() == 0) {
//不包含,需要移除
children.remove(i);
}else {
//包含,當前節點不需要移除,處理其子節點串列
item.arrange();
}
}
}
/**
*
* @methodName : getNodeList
* @description : 獲取包括自身及所有子節點的串列
* @param nodeList : 樹節點串列,入口引數為null
* @return : 樹節點串列
*
*/
public List<TreeNode<T>> getNodeList(List<TreeNode<T>> nodeList){
if (nodeList == null) {
//如果入口節點,則引數為null,需要創建
nodeList = new ArrayList<TreeNode<T>>();
}
//加入自身節點
nodeList.add(this);
//加入所有子節點
for(int i = 0; i < children.size(); i++) {
TreeNode<T> childNode = children.get(i);
childNode.getNodeList(nodeList);
}
return nodeList;
}
/**
*
* @methodName : toString
* @description : 多載toString方法
* @return : 回傳序列化輸出的字串
*
*/
@Override
public String toString() {
String sRet = "";
//根節點的資料部分不必輸出
if (parent != null) {
//非根節點
//輸出節點開始符號
sRet = "{";
//輸出isIncluded值,剪枝后的樹,無需輸出此欄位
//sRet += "\"isIncluded\":" + isIncluded + ",";
//輸出當前節點資料
sRet += "\"nodeData\":" + nodeData.toString();
//與前一個節點分隔
sRet += ",";
sRet += "\"children\":";
}
//輸出子節點
//子節點串列
sRet += "[";
String sChild = "";
//遍歷子節點
for(TreeNode<T> item : children) {
//輸出子節點資料
if (sChild.equals("")) {
sChild = item.toString();
}else {
sChild += "," + item.toString();
}
}
sRet += sChild;
//結束串列
sRet += "]";
if (parent != null) {
//輸出節點結束符號
sRet += "}";
}
return sRet;
}
}
TreeNode類提供下列介面方法:
- addChildNode方法,添加一個子節點,
- removeChildNode方法,洗掉一個子節點,
- getPrevSibling方法,取得左鄰節點,
- getNextSibling方法,取得右鄰節點,
- lookUpSubNode方法,在當前節點及下級節點中查找指定節點ID的節點,
- lookUpSuperNode方法,在當前節點及上級節點中查找指定節點ID的節點,
- clone方法,復制當前樹節點表示的樹或子樹,
- setChildrenIsIncluded方法,設定全部下級節點的isIncluded屬性值,
- combineTreeNode方法,將結構完全相同的節點合并到本節點中,合并后的節點的isIncluded屬性作位或運算,兩棵樹合并,用完全相同結構的樹合并要比不同結構的樹合并要方便很多,如多種角色組合的權限樹,先用全樹合并,然后再剪枝,會方便很多,
- arrange方法,對樹進行剪枝處理,即所有isIncluded為0的節點,都被移除,
- getNodeList方法,將所有節點物件(包含自身節點及所有下級節點),輸出到串列中,便于外部進行遍歷訪問,由于樹的遍歷,需要遞回,外部不好訪問,
- toString方法,實作Serializable介面類需要多載的方法,提供樹結構資料的序列化輸出,
5、樹結構的節點資料類示例
? 樹結構的節點資料,以權限管理的功能權限樹為例,物體類為FunctionInfo,代碼如下:
package com.abc.questInvest.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Id;
import com.abc.questInvest.vo.ITreeNodeData;
import lombok.Data;
/**
* @className : FunctionInfo
* @description : 功能節點資訊
*
*/
@Data
public class FunctionInfo implements Serializable,ITreeNodeData {
private static final long serialVersionUID = 1L;
//功能ID
@Id
@Column(name = "func_id")
private Integer funcId;
//功能名稱
@Column(name = "func_name")
private String funcName;
//父節點ID
@Column(name = "parent_id")
private Integer parentId;
//功能所在層級
@Column(name = "level")
private Byte level;
//顯示順序
@Column(name = "order_no")
private Integer orderNo;
//訪問介面url
@Column(name = "url")
private String url;
//dom物件的id
@Column(name = "dom_key")
private String domKey;
// ================ 介面多載 ======================
//獲取節點ID
@Override
public int getNodeId() {
return funcId;
}
//獲取節點名稱
@Override
public String getNodeName() {
return funcName;
}
//獲取父節點ID
@Override
public int getParentId() {
return parentId;
}
//物件克隆
@Override
public Object clone(){
FunctionInfo obj = null;
try{
obj = (FunctionInfo)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return obj;
}
@Override
public String toString() {
return "{"
+ "\"funcId\":" + funcId + ","
+ "\"funcName\":\"" + funcName + "\","
+ "\"parentId\":" + parentId + ","
+ "\"level\":" + level + ","
+ "\"orderNo\":" + orderNo + ","
+ "\"url\":\"" + url + "\","
+ "\"domKey\":\"" + domKey + "\""
+ "}";
}
}
FunctionInfo類對應資料庫的功能樹表function_tree,表結構如下:
DROP TABLE IF EXISTS `function_tree`;
CREATE TABLE `function_tree`
(
`func_id` INT(11) NOT NULL DEFAULT 0 COMMENT '功能ID',
`func_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '功能名稱',
`parent_id` INT(11) NOT NULL DEFAULT 0 COMMENT '父功能ID',
`level` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '功能所在層級',
`order_no` INT(11) NOT NULL DEFAULT 0 COMMENT '顯示順序',
`url` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '訪問介面url',
`dom_key` VARCHAR(80) NOT NULL DEFAULT '' COMMENT 'dom物件的id',
`remark` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '備注',
-- 記錄操作資訊
`operator_name` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '操作人賬號',
`delete_flag` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '記錄洗掉標記,1-已洗掉',
`create_time` DATETIME(3) NOT NULL DEFAULT NOW(3) COMMENT '創建時間',
`update_time` DATETIME(3) DEFAULT NULL ON UPDATE NOW(3) COMMENT '更新時間',
PRIMARY KEY (`func_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8 COMMENT ='功能表';
6、功能樹資料服務
6.1、Dao類
? Dao類為FunctionTreeDao,代碼如下:
package com.abc.questInvest.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import com.abc.questInvest.entity.FunctionInfo;
/**
* @className : FunctionTreeDao
* @description : function_tree表資料訪問類
*
*/
@Mapper
public interface FunctionTreeDao {
//查詢所有功能樹表記錄,按parent_id,order_no排序
@Select("SELECT func_id,func_name,parent_id,level,order_no,url,dom_key"
+ " FROM function_tree ORDER BY parent_id,order_no")
List<FunctionInfo> selectAll();
}
? 注意,查詢資料需要按parent_id,order_no排序,且資料中,func_id要比按parent_id的值大,從而可以實作樹結構的正常加載,
6.2、Service類
? Service類為FunctionTreeService,代碼如下:
package com.abc.questInvest.service;
import com.abc.questInvest.entity.FunctionInfo;
import com.abc.questInvest.vo.TreeNode;
/**
* @className : FunctionTreeService
* @description : 功能樹服務
*
*/
public interface FunctionTreeService {
/**
*
* @methodName : loadData
* @description : 加載資料庫中資料
* @return : 成功回傳true,否則回傳false,
*
*/
public boolean loadData();
/**
*
* @methodName : getFunctionTree
* @description : 獲取整個功能樹
* @return : 完整的功能樹
*
*/
public TreeNode<FunctionInfo> getFunctionTree();
}
6.3、ServiceImpl類
? Service實作類為FunctionTreeServiceImpl,代碼如下:
package com.abc.questInvest.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.abc.questInvest.dao.FunctionTreeDao;
import com.abc.questInvest.entity.FunctionInfo;
import com.abc.questInvest.service.FunctionTreeService;
import com.abc.questInvest.vo.TreeNode;
import lombok.extern.slf4j.Slf4j;
/**
* @className : FunctionTreeServiceImpl
* @description : FunctionTreeService實作類
*
*/
@Slf4j
@Service
public class FunctionTreeServiceImpl implements FunctionTreeService {
//function_tree表資料訪問物件
@Autowired
private FunctionTreeDao functionTreeDao;
//功能樹物件
private TreeNode<FunctionInfo> functionTree;
/**
*
* @methodName : loadData
* @description : 加載資料庫中資料
* @return : 成功回傳true,否則回傳false,
*
*/
@Override
public boolean loadData() {
try {
//查詢function_tree表,獲取全部資料
List<FunctionInfo> functionInfoList = functionTreeDao.selectAll();
//創建根節點
functionTree = new TreeNode<FunctionInfo>();
functionTree.setParent(null);
//創建空節點資料
functionTree.setNodeData(new FunctionInfo());
//約定根節點的節點ID為0
functionTree.getNodeData().setFuncId(0);
//根節點總是包含的
functionTree.setIsIncluded(1);
//將查詢結果放入functionTree物件中,按照樹的結構組織
//當前節點
TreeNode<FunctionInfo> treeNode = null;
//前一個節點的父節點,考慮到同一個父節點下的子節點物件是連續出現的
//記下前一個節點的父節點,可以減少節點搜索次數
TreeNode<FunctionInfo> preNodeParent = null;
for(FunctionInfo item : functionInfoList) {
//生成樹節點
treeNode = new TreeNode<FunctionInfo>();
//設定節點資料
treeNode.setNodeData(item);
//如果前一節點的父節點不為空
if (preNodeParent != null) {
//比較當前父節點ID與前一節點的父節點的節點ID
if(preNodeParent.getNodeData().getNodeId() == item.getParentId()) {
//如果相等,表示父節點沒有變化,繼續加入此父節點
preNodeParent.addChildNode(treeNode);
}else {
//如果父節點ID不同,則表示為新的父節點,從根節點向下搜索新的父節點
preNodeParent = functionTree.lookUpSubNode(item.getParentId());
if (preNodeParent != null) {
//如果找到父節點,則加入
preNodeParent.addChildNode(treeNode);
}else {
//指定父節點ID不在已有樹中,資料加載次序錯誤,或資料錯誤
log.error("FunctionTreeServiceImpl.loadData error with func_id = " + item.getFuncId());
return false;
}
}
}else {
//第一個子節點
functionTree.addChildNode(treeNode);
preNodeParent = functionTree;
}
}
}catch(Exception e) {
log.error(e.getMessage());
e.printStackTrace();
return false;
}
return true;
}
/**
*
* @methodName : getFunctionTree
* @description : 獲取整個功能樹
* @return : 完整的功能樹
*
*/
@Override
public TreeNode<FunctionInfo> getFunctionTree(){
return functionTree;
}
}
6.4、單元測驗
? 對FunctionTreeService使用單元測驗,觀察效果,代碼如下:
/**
* @className : QuestInvestApplicationTest
* @description : 啟動測驗類
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class QuestInvestApplicationTest {
@Autowired
ServletContext servletContext;
@Autowired
FunctionTreeService functionTreeService;
@Test
public void functionTreeServiceTest() {
boolean bRet = false;
bRet = functionTreeService.loadData();
if (bRet) {
TreeNode<FunctionInfo> functionTree = functionTreeService.getFunctionTree();
System.out.println(functionTree);
}
}
}
執行測驗代碼,可以看到輸出的功能樹資料,將之用網上的JSON查看器查看,可以看到下圖的樹型結構:

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/288350.html
標籤:Java
下一篇:Netty 框架學習 —— 引導
