第一部分:问题抛出
常见的设计,采用标准的树形结构,每个结点记录父ID(pid)。利用pid查询子集时,一次只能查询出一层,查询多层时,逻辑代码将会非常繁琐,而且无法一次查询出子集的数量等等,另外多次查询效率不高。
推荐一种数据结构,大部分情况两次查询,可找出所有需要的数据,减少查询次数,提升性能,本质也是树。可应用于评论、用户裂变、地域、分类等,可以支持无限层级。
第二部分:原理介绍
基本结构
每个节点有三个属性,节点名称、左属性、右属性。其中左属性、右属性为关键属性。
public class Node {
private String name;
private int lft;
private int rght;
}
规律
- 祖先节点的左 < 后代节点的左;祖先节点的右 > 后代节点的右;
- 节点(I)左右分别是6、7,其父节点(E)的左右分别是5、8;
- 节点(C)左右分别是12、17,其子节点(G)左右分别是13、14,子节点(H)是15、16;
- 节点(B)左右为2、11,其后代节点(D、E、F、I)中最小的左是3,最大的右是10;
- 节点(A)左右为1、18,其后代节点(B、C、D、E、F、G、H、I)中最小的左是2,最大的右是17;
- 同一节点右左之差,除以2并向下取整,即为后代节点数量;
- 节点(I)的后代节点数量 = ( 7 - 6 ) / 2 = 0;
- 节点(E)的后代节点数量 = ( 8 - 5 ) / 2 = 1;
- 节点(B)的后代节点数量 = ( 11 - 2 ) / 2 = 4;
- 节点(A)的后代节点数量 = ( 18 - 1 ) / 2 = 8;
找后代节点
- 如找节点(E)的后代节点,条件:左 > 5 AND 右 < 8,结果是节点(I);
- 如找节点(B)的后代节点,条件:左 > 2 AND 右 < 11;结果是结点(D、E、F、I);
- 如找节点(A)的后代节点,条件:左 > 1 AND 右 < 18;结果是结点(B、C、D、E、F、G、H、I);
如果查询的结果也想包含自己,则上述条件换成 >= 和 <=
找祖先节点
- 如找节点(B)的祖先节点,条件:左 < 2 AND 右 > 11,结果是节点(A);
- 如找节点(E)的祖先节点,条件:左 < 5 AND 右 > 8;结果是(A、B);
- 如找节点(I)的祖先节点,条件:左 < 6 AND 右 > 7;结果是(A、B、E);
如果查询的结果也想包含自己,则上述条件换成 <= 和 >=
增加节点(单个节点)
假设在节点(E)下增加子节点(J),注意只能加到最后(可以理解为,他最晚出生,他是最小的弟弟,其他的都是哥哥)。
- 为新增节点(J)设置左右属性,左 = 父节点的右;右 = 左 + 1;
- 修改祖先节点的右属性,右 = 右 + 2 WHERE 左 < J(左) && 右 >= J(左),这里涉及到节点(A、B、E);
- 修改右侧节点的左右属性,SET 左 = 左 + 2, 右 = 右 + 2 WHERE 左 >= J(右),这里涉及节点(F、G、H、C);
变化如下:
插入已完成,可以尝试着把示例图完整画一遍。注意,除第一个节点(A)的左右手动设置外,其余的节点的都是动态算出来的。示例中左是从1开始,可以使用任何一个自然数,如:-1、3678、7758、10000等等;
删除节点(单个节点)
如上例中,删除结点(D)
- 步骤如下:
- 修改祖先节点的右属性,SET 右 = 右 - 2 WHERE 左 < D(左) AND 右 > D(右),这里涉及到结点(A、B);
- 修改右侧节点左右属性,SET 左 = 左 - 2, 右 = 右 - 2 WHERE 左 > D(右),这里涉及节点(E、I、J、F、G、H、C);
变化如下:
物理删除节点(带子节点的节点)
利用递归,先删除子节点,再删除自己。(真实的使用场景中,建议使用逻辑删除,不做物理删除)
伪代码如下:
public void del(node) {
List childs = node.childs() // 包含自己,按左属性降序;
if(childs != empty) {
for;;
del(childs[i]);
return;
}
// 物理删除结点
}
更换节点(单个节点)
先做删除,再做添加,如上图把节点J挂到H下,
伪代码如下:
del(J);
add(J, H);
}
上面是基础的增删操作,下面介绍几种复杂的情况,主要还是利用循环或递归,执行删除、添加操作;
更换节点(带子节点)
如把E节点,挂在C下,推荐的做法是先删除节点,再添加;伪代码如下:
List childs = E.childs(); // 包含自己,注意排序为:[E, I, J]
for (int i = childs.length - 1; i >= 0; i--) {
// 注意这里是倒序删除,del(J)、del(I)、del(E)
del(childs.get(i));
}
// 注意这里是正序添加
add(E, C);
add(I, E);
add(J, E);
添加节点(插入指定位置)
如想把新节点K挂在C下,并且在G和H之间。上面得知,新加的节点,只能加到最后,这种情况的处理,伪代码如下:
add(K, C);
del(H);
add(H, C