前言
这一周是比较充实的一周,主要学习了解数据结构中树这一知识点。具体的业务场景是这样的:数据库里有大学的专业分类信息,需要我提供一个查询结构,以树的结构返回给前端。
问题描述
数据库结构如下:
对于大学专业信息表,需要提供一个查询接口,以树型的结构返回,结构如下:
[
{"id":"1810504230848405505",
"name": "经济学",
"children": [
{
"id": "1810504238213603330",
"name": "经济学类",
"children": [
{
"id": "1810504242995109889",
"name": "税务",
"children": []
},
{
"id": "1810504239270567937",
"name": "国际经济与贸易",
"children": []
}],
{},{}
]
问题分析
每个节点通过保存parentId来构建起这个树型结构,其中冗余了层级关系level字段,可以用于提高查询效率。下面就针对有冗余level字段和没有冗余时分别的解决方案。
解决方案
解决方案1
思路
这是组长最开始的写法,有level字段进行排序。首先初始化数据结构:创建一个 ArrayList,用于存储树的根节点。创建一个 HashMap,用于存储所有的 MajorNode 对象,其中键是节点的 ID,值是对应的 MajorNode 对象。第二步就是根据查询level 升序排序获取未删除的记录,然后构建节点,设置 MajorNode 的 id 和 name 属性。最后是确定层级关系:检查每个 MajorNode 的 parentId。如果 parentId 为 null,则表示该节点是根节点,将其添加到 root 列表中。如果 parentId 不为 null,则在 map 中查找对应的父节点,并把当前节点添加到父节点的children列表中。
public List<MajorNode> majorTree() {
List<MajorNode> root = new ArrayList<>();
Map<Long, MajorNode> map = Maps.newHashMap();
List<WyMetaMajor> list = this.lambdaQuery().eq(WyMetaMajor::getIsDelete, false).orderByAsc(WyMetaMajor::getLevel).list();
for (WyMetaMajor wyMetaMajor : list) {
MajorNode majorNode = new MajorNode();
majorNode.setId(wyMetaMajor.getId());
majorNode.setName(wyMetaMajor.getName());
Long parentId = wyMetaMajor.getParentId();
map.put(majorNode.getId(), majorNode);
if (Objects.isNull(parentId)) {
root.add(majorNode);
} else {
MajorNode parentNode = map.get(parentId);
parentNode.getChildren().add(majorNode);
}
}
return root;
}
总结
使用了 HashMap 来存储节点,这样可以在 O(1) 时间复杂度内访问任何节点,提高了效率。代码假设每个节点最多只有一个父节点,这符合一般树状结构的特点。代码没有处理 parentId 存在但未在 map 中找到对应节点的情况,这可能会导致某些节点丢失或树结构不完整。
解决方案2
思路
这一种方案是基于没有level字段排序的情况下,如何对层级关系进行一个构建,因为设计师在设计表的时候可能不会考虑到设计层级字段,那这样的话就需要我们对父Id进行一个查找匹配,并判断构建树结构。
public List<MajorNode> buildMajorTree() {
List<MajorNode> root = new ArrayList<>();
Map<Long, MajorNode> map = Maps.newHashMap();
// 假设这里一次性查询了所有WyMetaMajor记录
List<WyMetaMajor> list = this.lambdaQuery().list();
// 初始化所有节点
for (WyMetaMajor wyMetaMajor : list) {
MajorNode majorNode = new MajorNode();
majorNode.setId(wyMetaMajor.getId());
majorNode.setName(wyMetaMajor.getName());
map.put(wyMetaMajor.getId(), majorNode);
}
// 构建树结构
for (WyMetaMajor wyMetaMajor : list) {
Long parentId = wyMetaMajor.getParentId();
MajorNode majorNode = map.get(wyMetaMajor.getId());
if (Objects.isNull(parentId)) {
// 如果没有父ID,则是根节点
root.add(majorNode);
} else {
// 如果有父ID,找到父节点并添加当前节点为子节点
MajorNode parentNode = map.get(parentId);
if (parentNode != null) {
parentNode.getChildren().add(majorNode);
}
}
}
return root;
}
总结
根据每个WyMetaMajor记录的parentId字段确定其在树中的位置。如果parentId为null,表示当前节点是根节点,将其添加到root列表中。如果parentId不为null,表示当前节点是某个父节点的子节点,通过map查找父节点,并将当前节点添加到父节点的children列表中。
解决方案3
思路
通过Hutool工具包提供的内置方法进行解决。
public List<Tree<Long>> selectTree() {
List<WyMetaMajor> list = this.lambdaQuery()
.eq(WyMetaMajor::getIsDelete, false)
.orderByAsc(WyMetaMajor::getLevel)
.list();
List<TreeNode<Long>> nodeList = CollUtil.newArrayList();
for (WyMetaMajor wyMetaMajor : list) {
// 创建树节点
TreeNode<Long> treeNode = new TreeNode<>();
treeNode.setId(wyMetaMajor.getId());
treeNode.setParentId(wyMetaMajor.getParentId());
treeNode.setName(wyMetaMajor.getName());
// 如果需要,设置额外的属性
Map<String, Object> extraMap = new HashMap<>();
extraMap.put(WyMetaMajor.Fields.id, wyMetaMajor.getName());
// 将WyMetaMajor对象转换为map作为额外信息
treeNode.setExtra(BeanUtil.beanToMap(wyMetaMajor));
nodeList.add(treeNode);
}
// 使用 Hutool 的 TreeUtil 构建树结构
return TreeUtil.build(nodeList, null);
}
总结
代码的关键点在于使用 Hutool 的 TreeUtil 类来简化树形结构的构建过程。他的底层逻辑也是对这些字段进行相应的处理,这种方法在处理具有父子关系的数据时非常有效,尤其是在构建组织结构、分类体系或其他层次化数据模型时。
拓展
由于以上代码都是建立在树结构上的,所以我特意去学习了普通树和二叉树的相关知识点。
一、树
- 结点的度(Degree): 一个节点含有的子树的个数称为该点的度
- 树的度: 一棵树中,最大的节点的度称为树的度
- 叶结点(Leaf): 度为0的结点. (也称为叶子结点)
- 路径和路径长度: 从结点n1到nk的路径为一个结点序列n1, n2,… , nk, ni是 ni+1的父结点。路径所包含边的个数为路径的长度。
- 结点的层次(Level): 规定根结点在1层,其它任一结点的层数是其父结点的层数加1。
- 树的高度或深度(Depth): 树中所有结点中的最大层次是这棵树的深度。
二、二叉树:
节点
起始节点:根节点
无孩子节点:叶子结点
遍历方式:
-
广度优先遍历(Breadth-first order):尽可能先访问距离根最近的节点,也称为层序遍历
-
深度优先遍历(Depth-first order):对于二叉树,可以进一步分成三种(要深入到叶子节点)
- pre-order 前序遍历,对于每一棵子树,先访问该节点,然后是左子树,最后是右子树
- in-order 中序遍历,对于每一棵子树,先访问左子树,然后是该节点,最后是右子树
- post-order 后序遍历,对于每一棵子树,先访问左子树,然后是右子树,最后是该节点
代码实现二叉树遍历方式:
树节点类
/**
* <h3>树节点类</h3>
* @author free
*/
public class TreeNode {
// 节点值
public int val;
// 左子节点
public TreeNode left;
// 右子节点
public TreeNode right;
public TreeNode(int val) {
this.val = val;
}
public TreeNode(TreeNode left, int val, TreeNode right) {
this.left = left;
this.val = val;
this.right = right;
}
@Override
public String toString() {
return String.valueOf(this.val);
}
}
递归实现
前、中、后序遍历,注意:构造树结构需要按照构造函数格式
package com.itheima.datastructure.binarytree;
public class TreeTraversal {
public static void main(String[] args) {
/*
1
/ \
2 3
/ / \
4 5 6
*/
TreeNode root = new TreeNode(
new TreeNode(new TreeNode(4), 2, null), 1,
new TreeNode(new TreeNode(5), 3, new TreeNode(6))
);
preOrder(root);
System.out.println();
inOrder(root);
System.out.println();
postOrder(root);
System.out.println();
}
/**
* <h3>前序遍历</h3>
* @param node 节点
*/
static void preOrder(TreeNode node) {
if (node == null) {
return;
}
System.out.print(node.val + "\t"); // 值
preOrder(node.left); // 左
preOrder(node.right); // 右
}
/**
* <h3>中序遍历</h3>
* @param node 节点
*/
static void inOrder(TreeNode node) {
if (node == null) {
return;
}
inOrder(node.left); // 左
System.out.print(node.val + "\t"); // 值
inOrder(node.right); // 右
}
/**
* <h3>后序遍历</h3>
* @param node 节点
*/
static void postOrder(TreeNode node) {
if (node == null) {
return;
}
postOrder(node.left); // 左
postOrder(node.right); // 右
System.out.print(node.val + "\t"); // 值
}
}
非递归实现
前序遍历
LinkedListStack<TreeNode> stack = new LinkedListStack<>();
TreeNode curr = root;
// 循环直到栈为空且当前节点为空
while (!stack.isEmpty() || curr != null) {
// 如果当前节点不为空
if (curr != null) {
// 将当前节点压入栈中
stack.push(curr);
// 打印当前节点
System.out.println(curr);
// 将当前节点移动到左子节点
curr = curr.left;
} else {
// 如果当前节点为空,从栈中弹出一个节点
TreeNode pop = stack.pop();
// 将当前节点设置为弹出节点的右子节点
curr = pop.right;
}
}
中序遍历
LinkedListStack<TreeNode> stack = new LinkedListStack<>();
TreeNode curr = root;
// 循环直到栈为空或当前节点为空
while (!stack.isEmpty() || curr != null) {
// 如果当前节点不为空
if (curr != null) {
// 将当前节点压入栈中
stack.push(curr);
// 将当前节点移动到左子节点
curr = curr.left;
} else {
// 如果当前节点为空,从栈中弹出一个节点
TreeNode pop = stack.pop();
// 打印弹出的节点
System.out.println(pop);
// 将当前节点设置为弹出节点的右子节点
curr = pop.right;
}
}
后序遍历
LinkedListStack<TreeNode> stack = new LinkedListStack<>();
TreeNode curr = root;
TreeNode pop = null;
// 循环直到栈为空或当前节点为空
while (!stack.isEmpty() || curr != null) {
// 如果当前节点不为空
if (curr != null) {
// 将当前节点压入栈中
stack.push(curr);
// 将当前节点移动到左子节点
curr = curr.left;
} else {
// 如果当前节点为空,查看栈顶元素
TreeNode peek = stack.peek();
// 如果栈顶元素没有右子节点,或者右子节点已访问过(即等于pop)
if (peek.right == null || peek.right == pop) {
// 从栈中弹出节点
pop = stack.pop();
// 打印弹出的节点
System.out.println(pop);
} else {
// 否则,将当前节点设置为栈顶元素的右子节点
curr = peek.right;
}
}
}
对于后序遍历,向回走时,需要处理完右子树才能 pop 出栈。如何知道右子树处理完成呢?
-
如果栈顶元素的 表示没啥可处理的,可以出栈
-
如果栈顶元素的 ,
- 那么使用 lastPop 记录最近出栈的节点,即表示从这个节点向回走
- 如果栈顶元素的 此时应当出栈
对于前、中两种遍历,实际以上代码从右子树向回走时,并未走完全程(stack 提前出栈了)后序遍历以上代码是走完全程了
三、普通树和二叉树的特性和区别:
普通树(General Tree)
普通树是一种层次结构的数据结构,它由节点组成,每个节点可以有任意数量的子节点。
特性:
- 节点数:普通树可以有任意数量的节点。
- 子节点:每个节点可以有多个子节点,没有限制。
- 根节点:普通树有一个唯一的根节点,它是树的起始点,没有父节点。
- 路径:从根节点到任意节点的路径是唯一的。
- 有序性:普通树的子节点通常是有序的,这意味着子节点的顺序是有意义的。
- 应用:普通树适用于表示具有层级关系的数据,如文件系统、组织结构图等。
二叉树(Binary Tree)
二叉树是普通树的一种特殊形式,它要求每个节点最多只能有两个子节点,通常称为左子节点和右子节点。
特性:
- 节点数:二叉树可以有任意数量的节点。
- 子节点:每个节点最多有两个子节点,分别是左子节点和右子节点。
- 根节点:二叉树有一个唯一的根节点,它是树的起始点,没有父节点。
- 有序性:二叉树的子节点是有序的,左子节点通常表示较小的值,右子节点表示较大的值(在二叉搜索树中)。
- 平衡性:二叉树可以是平衡的或非平衡的。平衡二叉树具有更好的性能特性,如 AVL 树和红黑树。
- 遍历方式:二叉树有多种遍历方式,包括前序遍历、中序遍历、后序遍历和层序遍历。
- 应用:二叉树广泛应用于实现二叉搜索树、表达式树、二叉堆等,用于排序、查找、数据压缩和任务调度等场景。
区别
普通树和二叉树的主要区别在于节点的子节点数量限制。普通树没有子节点数量的限制,而二叉树限制每个节点最多有两个子节点。这使得二叉树在某些操作和算法设计上具有特定的优势,如二叉搜索树的快速查找特性。然而,普通树由于其灵活性,在表示复杂的层级关系时更为通用。