左右值编码树状数据存储方案(java实现森林算法)
我们在日常的学习工作中经常会用树状的结构来表示某些数据集的关联关系,比如上下级部门、各种各样的分类等等。而我们通常使用的关系型数据库是使用二维表来存储数据的,因此就需要我们设计合理的schema来存储树形结构。
数据准备
首先我们准备一组数据来进行展示如何将树状的数据存储至关系型数据库:
继承关系设计
将上图所示的数据以二维表的形式存储起来,最简单的方式就是分析节点之间的继承关系,通过父节点id的方式将数据的继承关系表示出来,如下表所示:
id | value | p_id |
1 | A | NULL |
2 | B | 1 |
3 | C | 1 |
4 | D | 2 |
5 | E | 2 |
… | … | … |
这种设计方式很直观的展现出了数据的继承关系。但是我们若想知道一个节点的全部继承关系,那就要通过频繁的递归来不断地获取每一层的父节点,递归过程不断地访问数据库,增加了IO的开销。当树的深度比较小时还可以通过缓存的方式来进行优化,但当树的深度不可控时,就无法保证查询的效率了。
左右值编码
通过上文的分析,继承关系的查询开销大,就是因为使用了递归的方式频繁操作数据库导致的,那么我们是否可以利用一个数值区间来确定某一结点的族谱从而避免使用递归?
我们可以对最开始的树状结构进行改造,对每个结点分别增加两个数值作为左右编码,最终的结构如下图所示:
我们先不考虑每个节点的左右值是如何计算出来的,我们先看这种设计的查询是否能达到我们预期的结果:
id | value | left | right |
1 | A | 1 | 20 |
2 | B | 2 | 13 |
3 | C | 14 | 19 |
4 | D | 3 | 4 |
5 | E | 5 | 10 |
… | … | … | … |
现在我们要查询J
节点的所有父节点:
- 我们先查出
J
节点的左右值:
SELECT t.`left`, t.`right`
FROM `TABLE` t
WHERE t.`value` = 'J'
'''
查询结果 : 8 9
'''
- 我们根据
J
节点的左右值查询整个继承关系:
SELECT
GROUP_CONCAT( DISTINCT t.`value` ORDER BY t.`left` SEPARATOR ',' )
FROM
`TABLE` t
WHERE
t.`left` <= 8 AND t.`right` >= 9;
'''
查询结果:A,B,E,G
'''
可以看出我们只需要根据查询节点的左右值就可以确定与其具有继承关系的所有节点的左右值的的取值区间,然后仅需一次查询就可以获取到该节点的整个族谱路径。
左右值构建过程
那么接下来我们看一下每个节点的左右值是如何构建的,从而了解我们应该如何查询节点之间的关系。
从上图可以看出我们左右值的构建顺序其实是与树(这里叫森林比较妥当)的先根遍历的思路是一致的,先构建根节点的左值,然后遍历构建子树,子树构建完成后,构建根节点的右值。
因此我们可以先使用节点继承的方式来构建数据,然后根据节点继承的方式将森林构建出来,通过森林的先根遍历补充左右值,部分代码如下:
节点
package structure.forest;
import java.util.LinkedList;
import java.util.List;
/**
* @author lansion
* @version v1.0
* @date 2022/1/9 18:45
*/
public class ForestNode<M> {
/**
* 当前节点数据
*/
private M data;
/**
* 父节点
*/
private ForestNode<M> parent;
/**
* 孩子节点
*/
private List<ForestNode<M>> children;
public M getData() {
return data;
}
public void setData(M data) {
this.data = data;
}
public ForestNode<M> getParent() {
return parent;
}
public void setParent(ForestNode<M> parent) {
this.parent = parent;
}
public List<ForestNode<M>> getChildren() {
return children;
}
public void setChildren(List<ForestNode<M>> children) {
this.children = children;
}
public void addChildren(ForestNode<M> cNode) {
if (null == children) {
List<ForestNode<M>> nodes = new LinkedList<>();
nodes.add(cNode);
this.children = nodes;
} else {
this.children.add(cNode);
}
}
}
森林
package structure.forest;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* @author lansion
* @version v1.0
* @date 2022/1/9 18:44
*/
public class Forest<M> {
/**
* 森林全部节点
*/
private final List<ForestNode<M>> forest;
/**
* 根节点集合
*/
private List<ForestNode<M>> rootNodes;
public Forest(List<ForestNode<M>> forest) {
this.forest = forest;
buildForest();
buildRootNodes();
}
public List<ForestNode<M>> getForest() {
return forest;
}
public List<ForestNode<M>> getRootNodes() {
return rootNodes;
}
/**
*
* 森林先序遍历
*
* @return {@link List<ForestNode<M>>} 森林先序序列
* @see Forest#getFirstRootList(List, List)
*/
public List<ForestNode<M>> getFirstRootList() {
List<ForestNode<M>> firstRootList = new LinkedList<>();
getFirstRootList(rootNodes, firstRootList);
return firstRootList;
}
public void getFirstRootList(List<ForestNode<M>> nodes, List<ForestNode<M>> firstRootList) {
for (ForestNode<M> node : nodes) {
firstRootList.add(node);
final List<ForestNode<M>> children = node.getChildren();
if (null != children && children.size() != 0) {
getFirstRootList(children, firstRootList);
}
}
}
/**
* 构建森林
*/
public void buildForest() {
for (ForestNode<M> node : forest) {
if (null != node.getParent()) {
final ForestNode<M> parent = node.getParent();
parent.addChildren(node);
}
}
}
public void buildRootNodes() {
rootNodes = new ArrayList<>();
for (ForestNode<M> node : forest) {
if (null == node.getParent()) {
rootNodes.add(node);
}
}
}
}
构建左右值
private static Long index = 1L;
/**
*
* 构建字典树
*
* @param sysTreeDictForest 待构建森林
* @return {@link List} 构建完成后的左右值数据
* @see this#generateIndex(List)
*/
private List<Data> createTree(Forest<Data> sysTreeDictForest) {
index = 1L;
List<Data> dictList = new LinkedList<>();
final List<ForestNode<Data>> forest = sysTreeDictForest.getForest();
final List<ForestNode<Data>> rootNodes = sysTreeDictForest.getRootNodes();
generateIndex(rootNodes);
for (ForestNode<Data> node : forest) {
dictList.add(node.getData());
}
return dictList.stream().sorted(Comparator.comparing(Data::getLeftIndex)).collect(Collectors.toList());
}
/**
*
* 维护左右值
*
* @param nodes 节点集合
*/
private void generateIndex(List<ForestNode<Data>> nodes) {
for (ForestNode<Date> node : nodes) {
final Data data = node.getData();
final List<ForestNode<Data>> children = node.getChildren();
data.setLeftIndex(index++);
if (null != children && children.size() != 0) {
generateIndex(children);
}
data.setRightIndex(index++);
}
}
通过左右值的构建过程我们可以看出,子节点的左值是大于父节点的左值,而子节点的右值是小于父节点的右值的,根据这个性质我们可以对当前节点的子节点与父节点进行各个维度的查询,这里不做过多介绍,接下来进行介绍左右值编码的CRUD操作。
查询
- 查询所有子节点:左值大于该节点左值,右值小于该节点右值
- 查询对应节点族谱路径:左值小于该节点左值,右值大于该节点右值
- 查询子节点个数:
(右值-左值+1)/2
叶子节点增加子树
如图所示,我们要在G
节点下增加一个子树,根据刚才左右节点的构建过程,我们可以将G
节点的右值作为新增子树根节点的左值,让后将新增子树的的左右值构建完成,让后将元森林节点中所有大于等于原G
节点的右值的编码加上 新增节点数 * 2
。
非叶子节点增加子树
非叶子节点增加子树时应当考虑,当前节点的子节点如何放置,根据实际情况判断是作为新子树的兄弟节点还是作为新子树的一部分,作为新子树的兄弟节点的过程与叶子节点增加子树类似,这里介绍作为新子树一部分时该如何构建。
- 将新增子树和原节点子树构建为一棵新树
- 将原子树根节点的左值作为新增子树根节点的左值进行左右值编码构建
- 将原树中大于等于新增子树根节点左值的节点左右值分别加
新增节点数*2
(原节点子树算作新增)
删除叶子节点
删除叶子节点与新增叶子节点类似,将所有大于要删除节点的左值的编码减去2 * 1
,同理删除子树的话,减去2*节点数
删除非叶子节点
删除非叶子节点时,这里考虑一种情况,将被删除节点的子节点都作为被删除节点的兄弟节点。
- 因为左右码值构建时是按照先根遍历的顺序进行构建的,这里将子节点都作为了兄弟节点进行操作,也就是先根遍历时少了一个中间的根节点,因此只需将被删除节点所有子节点的的左右值加一
- 因为少了一个非叶子节点,也就是少了一对左右值,因此将大于被删除节点右值的编码全部减2。
节点的修改操作可以通过删除和新增进行实现,这里不过多叙述。
总结:
通过继承关系设计的schema,便于增删改,不便于查询
通过左右值编码设计的schema,便于查询不便于维护
db38269e-9a92-45ce-be56-ae6a64335fcd