在IT的项目开发中,有时我们会经常遇到树形列表数据的查询等的相关操作,比如下面的情况就是如此:
树形列表数据最基本的展示一共需要三个字段,分别是id,pId以及name。其中id是该条信息的主键,一般是自增的,并且不允许重复。pId是该条信息所归属的父节点的id,该pId允许重复,而顶级的节点其由于没有父节点,故其父节点字段往往以0进行标识。而name就是该条数据所展示的名称字段。
传统的树形列表数据的查询方式是先查询pId为0的数据,然后以该数据的id作为子节点pId的查询条件,用此来查询其直接的子节点数据;再以此子节点的id作为下一层数据的pId查询条件,依次类推,逐渐完成整个树形列表数据的查询操作。
源码如下:
Tree
package com.thinkgem.jeesite.entity;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@Builder
@ToString
public class Tree {
private int id;
private String name;
private int pId;
private String level;
}
TraditionTest
package com.thinkgem.jeesite.tree;
import com.baishun.common.utils.ListContainer;
import com.google.common.collect.Lists;
import com.thinkgem.jeesite.entity.Tree;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
/**
* 传统的查找方式
*/
@Slf4j
public class TraditionTest {
private ListContainer treeContainer = ListContainer.newListContainer();
/**
* 链式调用添加数据
*/
@Before
public void initDB(){
Tree tree = Tree.builder()
.id(1)
.name("0")
.pId(0)
.build();
Tree tree1 = Tree.builder()
.id(2)
.name("0-1")
.pId(1)
.build();
Tree tree2 = Tree.builder()
.id(3)
.name("0-2")
.pId(1)
.build();
Tree tree3 = Tree.builder()
.id(4)
.name("0-1-1")
.pId(2)
.build();
treeContainer.add(tree).add(tree1).add(tree2).add(tree3);
}
@Test
public void test(){
List treeList = treeContainer.getList();
long start = new DateTime().getMillis();
int i = 10000;
while(i > 0){
//遍历树节点
traverse(treeList);
i --;
}
long end = new DateTime().getMillis();
log.info("传统遍历耗时:" + (end - start));
}
/**
* 遍历树节点
* @param treeList
*/
public void traverse(List treeList){
List rootList = Lists.newArrayList();
treeList.forEach(tree -> {
//打印出父节点
if(tree.getPId() == 0){
/*log.info(tree.toString());*/
rootList.add(tree);
}
});
//从根节点开始循环遍历树节点
rootList.forEach(tree -> {
traverseChildren(tree,treeList);
});
}
/**
* 递归调用,不断的循环遍历
* @param tree
* @param treeList
*/
public void traverseChildren(Tree tree,List treeList){
treeList.forEach(tree1 -> {
if(tree1.getPId() == tree.getId()){
/*log.info(tree1.toString());*/
//进行循环递归调用
traverseChildren(tree1,treeList);
}
});
}
}
快速遍历的原理就是根据所传入的节点来查询该节点下的所有子节点信息,其中用到了一个关键的字段就是level,该字段显示的是当前的信息层级,根据level我们可以快速的查询该层级下的所有子节点信息,其中用到了另一个关键的容器,那就是Multimap,其特点就是允许在其中以Map,但是主键可以重复的方式存放信息。这样我们如果想获取某一层级下的子节点集合,我们直接调用multimap.get()方法即可,这样就省去了传统的不断循环遍历的操作。
源码如下:
QuickTest
package com.thinkgem.jeesite.tree;
import com.baishun.common.utils.ListContainer;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.thinkgem.jeesite.entity.Tree;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
/**
* 快速查找
* 两种查找的执行耗时比较
* 传统 快速
* 140 156
* 124 125
* 173 234
* 结论,如果要是执行遍历操作,那么采用传统的方式耗时更短
* 但是快速查找之所以被称之为快速,其主要的原因在于查找某一个节点下的所有子节点(非根节点,同时树中的节点比较多的情况下)
* 快速查找由于有辅助字段level的帮助,每一次的查找,其范围都要缩小一个层级,但是传统的查找并不能这样,传统的查找方式仍然
* 是每次查找一个节点,都要将全部的节点都扫描一遍,因而在这种情况下,快速查找方式要优于传统的查找方式。
* 因而在树形列表的查询使用中,推荐使用快速查找方式。
*/
@Slf4j
public class QuickTest {
private ListContainer treeContainer = ListContainer.newListContainer();
/**
* 链式调用添加数据
*/
@Before
public void initDB(){
Tree tree = Tree.builder()
.id(1)
.name("0")
.pId(0)
.level("0")
.build();
Tree tree1 = Tree.builder()
.id(2)
.name("0-1")
.pId(1)
.level("0.1")
.build();
Tree tree2 = Tree.builder()
.id(3)
.name("0-2")
.pId(1)
.level("0.1")
.build();
Tree tree3 = Tree.builder()
.id(4)
.name("0-1-1")
.pId(2)
.level("0.1.2")
.build();
treeContainer.add(tree).add(tree1).add(tree2).add(tree3);
}
@Test
public void test(){
List treeList = treeContainer.getList();
Tree tree = Tree.builder()
.id(1)
.name("0")
.pId(0)
.level("0")
.build();
long start = new DateTime().getMillis();
int i = 10000;
while(i > 0){
//遍历树节点
traverse(tree,treeList);
/*log.info(tree.toString());*/
i --;
}
long end = new DateTime().getMillis();
log.info("快速遍历耗时:" + (end - start));
}
/**
* 遍历树节点
* @param treeList
*/
public void traverse(Tree rootTree, List treeList){
List rootList = Lists.newArrayList();
Multimap multimap = ArrayListMultimap.create();
treeList.forEach(tree -> {
//将treeList中的信息放入到multimap容器中
if(tree.getLevel().indexOf(getNextLevel(rootTree)) != -1){
multimap.put(tree.getLevel(),tree);
}
if(tree.getLevel().equals(getNextLevel(rootTree))){
rootList.add(tree);
}
});
//从根节点开始循环遍历树节点
rootList.forEach(tree -> {
/*log.info(tree.toString());*/
traverseChildren(tree,multimap);
});
}
/**
* 递归调用,不断的循环遍历
* @param rootTree
* @param multimap
*/
public void traverseChildren(Tree rootTree, Multimap multimap){
String nextLevel = getNextLevel(rootTree);
List treeList = (List) multimap.get(nextLevel);
Multimap multimapChildren = ArrayListMultimap.create();
List rootList = Lists.newArrayList();
treeList.forEach(tree1 -> {
//将treeList中的信息放入到multimap容器中
if(tree1.getLevel().indexOf(getNextLevel(rootTree)) != -1){
multimapChildren.put(tree1.getLevel(),tree1);
}
if(tree1.getLevel().equals(getNextLevel(rootTree))){
rootList.add(tree1);
}
});
//从根节点开始循环遍历树节点
rootList.forEach(tree -> {
/*log.info(tree.toString());*/
traverseChildren(tree,multimapChildren);
});
}
/**
* 获取下一层级的level
* @param tree
* @return
*/
private String getNextLevel(Tree tree){
return tree.getLevel() + "." + tree.getId();
}
}
以简单的遍历所有的节点为例:
执行次数 | 传统 | 快速 |
---|---|---|
一 | 140 | 156 |
二 | 124 | 125 |
三 | 173 | 234 |
通过上述比较,我们不难看出,在遍历全部的树形列表的时候,传统的查询方式是速度比较快的,而采用快速遍历的方式稍微要慢一些。这主要是由于快速遍历过程中多了一些容器的创建,比如说Multimap,以及字符串的拼接查找操作,比如说getNextLevel()以及indexOf()。
但是这并不是说明快速遍历方式总是比较慢。如果要是执行全部节点的遍历操作,那么采用传统的方式耗时更短但是快速遍历之所以被称之为快速,其主要的原因在于其在查找某一个节点下的所有子节点(非根节点,同时树中的节点比较多的情况下)快速遍历由于有辅助字段level的帮助,每一次的遍历,其搜索范围都要缩小一个层级,但是传统的遍历并不能这样,传统的遍历方式仍然是每次查找一个节点,都要将全部的节点都扫描一遍,因而在这种情况下,快速遍历方式要优于传统的遍历方式。因而在树形列表的遍历使用中,推荐使用快速遍历方式。