一、树结构介绍
tree: [
{
id: '1',
title: '节点1',
children: [
{ id: '1-1', title: '节点1-1' },
{ id: '1-2', title: '节点1-2' }
]
},
{
id: '2',
title: '节点2',
children: [{ id: '2-1', title: '节点2-1' }]
}
]
- 子树:当前节点具有children属性,并且children的长度大于0
- 叶子节点:当前节点没有children或者children的长度为0
二、树结构的遍历方法
树结构的常用场景之一就是遍历,遍历分为广度优先遍历和深度优先遍历。
深度优先遍历是可递归的
- 深度优先遍历又分为先序遍历和后续遍历
- 二叉树中还有中序遍历,可以是递归,也可以是循环
广度优先遍历是非递归的,通常用循环来实现
1、广度优先和深度优先的区别
- 广度优先:访问树的第n+1层前必须先访问完第n层
- 深度优先:访问完一颗子树再去访问后面的子树,而访问子树的时候,先访问根再访问根的子树,称为先序遍历;先访问子树再访问根,称为后续遍历
2、广度遍历的实现
const wide = tree => {
for (const item of tree) {
console.log(item.title)
item.children && tree.push(...item.children)
}
}
wide(tree)
打印结果:第一层元素会在第二层元素之前输出
节点1
节点2
节点1-1
节点1-2
节点2-1
while循环的写法
const wide = tree => {
let item
while ((item = tree.shift())) {
console.log(item.title)
item.children && tree.push(...item.children)
}
}
3、深度优先遍历的实现(递归)
- 先序遍历
const firstOrder = tree => {
for (const item of tree) {
console.log(item.title)
item.children && firstOrder(item.children)
}
}
firstOrder(tree)
打印结果:先访问根再访问根的子树
节点1
节点1-1
节点1-2
节点2
节点2-1
- 后序遍历
const lastOrder = tree => {
for (const item of tree) {
item.children && lastOrder(item.children)
console.log(item.title)
}
}
lastOrder(tree)
打印结果:先访问根的子树再访问根
节点1-1
节点1-2
节点1
节点2-1
节点2
4、深度优先遍历实现(循环)
- 先序遍历:和广度优先循环实现类似,要维护一个队列,不同的是子节点不是追加到队列最后,而是加到队列最前面
const firstOrder = tree => {
let item
while ((item = tree.shift())) {
console.log(item.title)
item.children && tree.unshift(...item.children)
}
}
firstOrder(tree)
打印结果:
节点1
节点1-1
节点1-2
节点2
节点2-1
- 后序遍历:
const lastOrder = tree => {
let item,
i = 0
while ((item = tree[i])) {
const childCount = item.children ? item.children.length : 0
if (!childCount || item.children[childCount - 1] === tree[i - 1]) {
console.log(item.title)
i++
} else {
tree.splice(i, 0, ...item.children)
}
}
}
lastOrder(tree)
打印结果:
节点1-1
节点1-2
节点1
节点2-1
节点2
三、列表和树结构相互转换
列表结构中一般在节点信息中给出父级id,以此作为依赖将列表转换为树
1、列表转为树
const list = [
{ id: '1', title: '节点1', parentId: '' },
{ id: '1-1', title: '节点1-1', parentId: '1', flag: false },
{ id: '1-2', title: '节点1-2', parentId: '1' },
{ id: '2', title: '节点2', parentId: '' },
{ id: '2-1', title: '节点2-1', parentId: '2' }
]
(1)第一种方式
const listToTree = list => {
const rootNode = list.filter(n => n.parentId === '')
const genChildren = parents =>
parents.map(parent => {
const children = list.filter(m => m.parentId === parent.id)
if (children.length) parent.children = genChildren(children)
return parent
})
return genChildren(rootNode)
}
(2)第二种方式
const listToTree = list => {
const info = list.reduce((acc, cur) => ((acc[cur.id] = cur), (cur.children = []), acc), {})
return list.filter(item => {
info[item.parentId] && info[item.parentId].children.push(item)
return !item.parentId
})
}
(3)第三种方式:只用循环一次,效率最高
const listToTree = (list) => {
const result = [] // 存放结果集
const itemMap = {}
for (const item of list) {
const { id, parentId } = item
if (!itemMap[id]) itemMap[id] = { children: [] }
itemMap[id] = { ...item, children: itemMap[id]['children'] }
const treeItem = itemMap[id]
if (parentId === '') {
result.push(treeItem)
} else {
if (!itemMap[parentId]) itemMap[parentId] = { children: [] }
itemMap[parentId].children.push(treeItem)
}
}
return result
}
(4)第四种方式
const listToTree = (list, id = '', arr = []) => {
for (const item of list) if (item.parentId === id) arr.push(item)
for (const item of arr) {
item.children = []
listToTree(list, item.id, item.children)
if (!item.children.length) delete item.children
}
return arr
}
2、树转为列表
(1)递归实现
const treeToList = (tree, result = [], level = 0) => {
for (const item of tree) {
result.push(item)
item.level = level + 1 // 添加层级属性
item.children?.length && treeToList(item.children, result, level + 1)
}
return result
}
(2)循环实现
const treeToList = tree => {
let node,
result = tree.map(node => ((node.level = 1), node))
for (const [i, item] of result.entries()) {
if (!item.children) continue
const list = item.children.map(
node => ((node.level = item.level + 1), node)
)
result.splice(i + 1, 0, ...list)
}
return result
}
四、树结构过滤
1、过滤掉不符合条件的节点:过滤掉flag为false的节点。函数中最好用将tree深复制再操作,否则会改变原树
const filterFalseByFlag = tree =>
tree.filter(item => { // tree需要深复制
if (item.children?.length)
item.children = filterFalseByFlag(item.children)
return item.flag !== false
})
2、递归为每层数据添加level
const setLevel = (tree, level = 1) =>
tree.map((item) => { // tree需要深复制
item.level = level
if (item.children?.length) item.children = setLevel(item.children, level + 1)
return item
})
递归为每层数据添加parentId
const setParentId = (tree, id = null) => {
tree = JSON.parse(JSON.stringify(tree))
for (const item of tree) {
item.parentId = id
if (item.children && item.children.length) item.children = setParentId(item.children, item.id)
}
return tree
}
3、过滤接口数据
接口数据(点击查看详细内容)
const tree = [
{
nodeId: '8d6d70cb44ea4172b4c6a3ab760330ea',
node: {
powerId: '8d6d70cb44ea4172b4c6a3ab760330ea',
parentId: '0',
orderIndex: 4,
resourceType: 'MENU',
resourceName: '项目管理',
level: null,
resourceDesc: '',
resourceUrl: '/projectManage',
resourceIcon: 'iconfont icon-xiangmu',
moduleId: 'ea3f471cc286450b8329e86e90fe24dc',
isDeleted: false,
createTime: '2020-06-22 18:45:26',
updateTime: '2020-06-22 18:52:36'
},
parentId: '0',
children: null,
childLeaf: true
},
{
nodeId: '6088a9af2f514e5090ccbd51110424b5',
node: {
powerId: '6088a9af2f514e5090ccbd51110424b5',
parentId: '0',
orderIndex: 6,
resourceType: 'MENU',
resourceName: '资料汇总',
level: null,
resourceDesc: '',
resourceUrl: '/dataCollection',
resourceIcon: 'iconfont icon-ziliao',
moduleId: '5ecf31302dff488f84620d5b816d98ee',
isDeleted: false,
createTime: '2020-06-22 18:45:26',
updateTime: '2020-06-22 18:52:56'
},
parentId: '0',
children: null,
childLeaf: true
},
{
nodeId: '8621da3397374510a17b59ad9cfdca74',
node: {
powerId: '8621da3397374510a17b59ad9cfdca74',
parentId: '0',
orderIndex: 9,
resourceType: 'MENU',
resourceName: '个人中心',
level: null,
resourceDesc: '',
resourceUrl: '/personalCenter',
resourceIcon: 'iconfont icon-yonghu1',
moduleId: '242026239a9e40879915867307364365',
isDeleted: false,
createTime: '2020-06-22 18:45:26',
updateTime: '2020-06-22 18:53:19'
},
parentId: '0',
children: null,
childLeaf: true
},
{
nodeId: '1b4c695c711640949872510e02752b69',
node: {
powerId: '1b4c695c711640949872510e02752b69',
parentId: '0',
orderIndex: 52,
resourceType: 'MENU',
resourceName: '伦理会议',
level: null,
resourceDesc: '',
resourceUrl: '/ethicalMeeting',
resourceIcon: '',
moduleId: '52ad856eb1a84fbbb6f31ba59f93aba4',
isDeleted: false,
createTime: '2020-06-22 18:45:26',
updateTime: '2020-06-22 18:45:26'
},
parentId: '0',
children: [
{
nodeId: '79234bc38316477680a555f3db4a00dd',
node: {
powerId: '79234bc38316477680a555f3db4a00dd',
parentId: '1b4c695c711640949872510e02752b69',
orderIndex: 1,
resourceType: 'MENU',
resourceName: '待会审项目',
level: null,
resourceDesc: '',
resourceUrl: "/ethicalMeeting/pendingReviewProject'",
resourceIcon: '',
moduleId: '52ad856eb1a84fbbb6f31ba59f93aba4',
isDeleted: false,
createTime: '2020-06-22 18:45:26',
updateTime: '2020-06-22 18:45:26'
},
parentId: '1b4c695c711640949872510e02752b69',
children: null,
childLeaf: true
},
{
nodeId: '5f38c08e24a64ba0bb9b3ee53f192eb2',
node: {
powerId: '5f38c08e24a64ba0bb9b3ee53f192eb2',
parentId: '1b4c695c711640949872510e02752b69',
orderIndex: 2,
resourceType: 'MENU',
resourceName: '会议记录',
level: null,
resourceDesc: '',
resourceUrl: '/ethicalMeeting/pendingReviewProject',
resourceIcon: '',
moduleId: '52ad856eb1a84fbbb6f31ba59f93aba4',
isDeleted: false,
createTime: '2020-06-22 18:45:26',
updateTime: '2020-06-22 18:45:26'
},
parentId: '1b4c695c711640949872510e02752b69',
children: null,
childLeaf: true
}
],
childLeaf: false
},
{
nodeId: 'a8a55825d32f43fb8dac59bf3aeef0e3',
node: {
powerId: 'a8a55825d32f43fb8dac59bf3aeef0e3',
parentId: '0',
orderIndex: 82,
resourceType: 'MENU',
resourceName: '用户管理',
level: null,
resourceDesc: '',
resourceUrl: '/userManage',
resourceIcon: 'iconfont icon-yonghuguanli',
moduleId: 'dc6804f659974d16bf5a498eceb65a25',
isDeleted: false,
createTime: '2020-06-22 18:45:26',
updateTime: '2020-06-22 18:45:26'
},
parentId: '0',
children: [
{
nodeId: 'fdb9c937156148cc88060450933e2d72',
node: {
powerId: 'fdb9c937156148cc88060450933e2d72',
parentId: 'a8a55825d32f43fb8dac59bf3aeef0e3',
orderIndex: 1,
resourceType: 'MENU',
resourceName: '本院用户',
level: null,
resourceDesc: '',
resourceUrl: '/userManage/ourHospitalUser',
resourceIcon: '',
moduleId: 'dc6804f659974d16bf5a498eceb65a25',
isDeleted: false,
createTime: '2020-06-22 18:45:26',
updateTime: '2020-06-22 18:45:26'
},
parentId: 'a8a55825d32f43fb8dac59bf3aeef0e3',
children: null,
childLeaf: true
}
],
childLeaf: false
}
]
递归处理:(只保留需要的数据)
const filter = (data, result = []) => {
result = data.map((item) => {
if (item.children) item.children = filter(item.children, [])
return {
path: item.node.resourceUrl,
text: item.node.resourceName,
icon: item.node.resourceIcon,
children: item.children || []
}
})
return result
}
效果:
使用reduce处理数据(filter和map的结合)
接口数据(点击查看详细内容)
[
{
"path": "/dashboard",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "icon-home",
"link": null,
"title": "首页"
},
"name": "/dashboard",
"id": 1135
},
{
"redirect": "noRedirect",
"component": "Layout",
"hidden": false,
"children": [
{
"path": "/org/adminoa",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "OA组织管理"
},
"name": "/org/adminoa",
"id": 1141
},
{
"path": "/org/admin",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "组织管理"
},
"name": "/org/admin",
"id": 1145
}
],
"meta": {
"noCache": false,
"icon": "icon-organization",
"link": null,
"title": "组织管理"
},
"id": 1136,
"alwaysShow": true
},
{
"redirect": "noRedirect",
"component": "Layout",
"hidden": false,
"children": [
{
"path": "/user/adminoa",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "OA用户管理"
},
"name": "/user/adminoa",
"id": 1155
},
{
"path": "/user/admin",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "用户管理"
},
"name": "/user/admin",
"id": 1156
}
],
"meta": {
"noCache": false,
"icon": "icon-user",
"link": null,
"title": "用户管理"
},
"id": 1137,
"alwaysShow": true
},
{
"redirect": "noRedirect",
"component": "Layout",
"hidden": false,
"children": [
{
"path": "/sys-admin/account",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "账号管理"
},
"name": "/sys-admin/account",
"id": 1168
},
{
"path": "/sys-admin/role",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "角色管理"
},
"name": "/sys-admin/role",
"id": 1169
},
{
"path": "/sys-admin/menu",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "菜单管理"
},
"name": "/sys-admin/menu",
"id": 1170
}
],
"meta": {
"noCache": false,
"icon": "icon-system",
"link": null,
"title": "系统管理"
},
"id": 1138,
"alwaysShow": true
},
{
"redirect": "noRedirect",
"component": "Layout",
"hidden": false,
"children": [
{
"path": "/log/loginlog",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "登录日志"
},
"name": "/log/loginlog",
"id": 1221
},
{
"path": "/log/operatelog",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "操作日志"
},
"name": "/log/operatelog",
"id": 1222
},
{
"path": "/log/interfacelog",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "接口日志"
},
"name": "/log/interfacelog",
"id": 1223
}
],
"meta": {
"noCache": false,
"icon": "icon-log",
"link": null,
"title": "日志管理"
},
"id": 1139,
"alwaysShow": true
},
{
"path": "/nationalDirectory",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "icon-address",
"link": null,
"title": "全国通讯录"
},
"name": "/nationalDirectory",
"id": 1211
},
{
"redirect": "noRedirect",
"component": "Layout",
"hidden": false,
"children": [
{
"path": "/policySettings/passwordSet",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "密码策略"
},
"name": "/policySettings/passwordSet",
"id": 1225
},
{
"path": "/policySettings/loginSet",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "",
"link": null,
"title": "登录登出策略"
},
"name": "/policySettings/loginSet",
"id": 1226
}
],
"meta": {
"noCache": false,
"icon": "icon-strategy",
"link": null,
"title": "策略配置"
},
"id": 1224,
"alwaysShow": true
},
{
"path": "/insertAdmin",
"component": "Layout",
"hidden": false,
"meta": {
"noCache": false,
"icon": "icon-join",
"link": null,
"title": "接入管理"
},
"name": "/insertAdmin",
"id": 1234
}
]
递归处理:
const filterData = data =>
data.reduce((acc, cur) => {
let { children, hidden, meta, path, id } = cur
if (children) children = filterData(children, [])
if (!hidden) {
const key = path || id
const { title: label } = meta
const icon = meta.icon && <i className={`iconfont ${meta.icon}`} />
acc.push({ label, key, path, icon, children })
}
return acc
}, [])
4、删除children为null或[]的children
const tree = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [{ value: 'xihu', label: 'West Lake' }]
}
]
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [{ value: 'nanjing', label: 'Nanjing', children: [] }]
},
{ value: 'anhui', label: 'anhui', children: null }
]
const recursionRemoveEmpty = (data) => {
data = JSON.parse(JSON.stringify(data)).filter((item) => {
if (item.children) item.children = recursionRemoveEmpty(item.children)
if (!item.children || (item.children && item.children.length === 0)) delete item.children
return item
})
return data
}
console.log(tree)
console.log(recursionRemoveEmpty(tree))
效果:
五、树结构查找
1、根据id查找当前节点
const getNodeById = (tree, id) => {
for (const item of tree) {
if (item.id === id) return item
if (item.children?.length) {
const node = getNodeById(item.children, id)
if (node) return node
}
}
}
2、根据id查找节点路径
const getNodePathById = (tree, id, path = []) => {
if (!tree) return []
for (const data of tree) {
path.push(data.id)
if (data.id === id) return path
if (data.children) {
const findChildren = getNodePathById(data.children, id, path)
if (findChildren.length) return findChildren
}
path.pop()
}
return []
}
const result = getNodePathById(tree, '2-1')
3、根据id查找当前节点和其所有的父级节点
const getParentsById = (tree, id) => {
for (const item of tree) {
if (item.children?.length) {
const nodeList = getParentsById(item.children, id)
if (nodeList) return nodeList.concat(item)
}
if (item.id === id) return [item]
}
}
4、根据当前节点找到其所有子级节点
const getNodeById = (tree, id) => {
for (const item of tree) {
if (item.id === id) return item
if (item.children?.length) {
const node = getNodeById(item.children, id)
if (node) return node
}
}
}
const node = getNodeById(tree, '1')
const childs = getChildsByNode([node])
5、获取树中所有的id集合
getIdsByTree (tree, result = []) {
for (const item of tree) {
item.id && result.push(item.id);
if (item.children) getIdsByTree(item.children, result);
}
return result;
};
六、尾调用优化
1、什么是尾调用
函数的最后一步是return调用另一个函数
function a() {
return b()
}
注意:以下几种情况不是尾调用
function a() {
return b() + 10 // 调用后又赋值
}
function a() {
b() // return 了undefined
}
function a() {
const bb = b()
return bb // 调用后还有操作
}
function a() {
return b() || c() // b()不是尾调用,c()是尾调用
}
等价于
function a() {
const bResult = b()
if (bResult) return bResult
return c()
}
这种的也是尾调用
function a(num) {
if (num > 10) {
return b(10)
} else {
return b(num)
}
return b()
}
2、尾调用的优点
- 调用栈的形成:如果在a函数的内部调用b函数,那么在a的调用记录上方,会产生一个b的调用记录。当b运行完返回结果到a,b的调用记录才会消失。如果b函数中还调用了c函数,那么在b的调用记录上方还有一个c的调用记录,以此类推,形成调用栈。栈内存遵循
先进后出,后进先出的原则,直到栈顶的执行完毕,才会释放之前行程的调用记录的内存。 - 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内部函数的调用记录
取代外层函数的调用记录就可以,所以尾调用的调用栈是恒定的,堆栈数没有随着函数调用的增加而增多。我们可以将一些不是尾调用的函数改写成尾调用的形式,达到优化调用堆栈的目的。
const a = () => {
const i = 1
const j = 2
return b(i + j)
}
a()
// 等价于
const a = () => {
return b(3)
}
a()
// 等价于
b(3)
解析:
- 上面代码中,如果b函数不是尾调用,函数a就需要保存内部变量i和j的值,b的调用位置等信息。但由于调用函数b后,函数a就结束了,所以执行到最后一步,完全可以删除函数a的调用记录,只保留b(3)的调用记录。
- 这就叫做
尾调用优化,即只保留内层函数的调用记录。如果所有的函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,内存将大大节省,这就是尾递归优化的意义。
3、尾递归
- 函数调用自身,称为
递归。如果尾调用自身,称为尾递归。 - 递归非常耗费内存,因为同时需要保存成百上千个调用记录,很容易发生
栈溢出错误(Uncaught RangeError: Maximum call stack size exceeded)。对于尾递归来说,由于只存在一个调用记录,所以永远不会发生栈溢出错误
递归求阶乘改写成尾递归调用,复杂度由O(n)降低为O(1):
// const factorial = (n) => {
// if (n === 1) return 1
// return n * factorial(n - 1)
// }
// 改成尾调用
const factorial = (n, total = 1) => {
if (n === 1) return total
return factorial(n - 1, n * total)
}
console.log(factorial(5)) // 120
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数factorial需要用到一个中间变量total,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,为什么计算5的阶乘,需要传入两个参数5和1?
可以在factorial函数外提供一个尾调用函数tailCall:
const factorial = (n) => {
return tailCall(n)
}
const tailCall = (n, total = 1) => {
if (n === 1) return total
return tailCall(n - 1, n * total)
}
console.log(factorial(5))
递归求和函数改写成尾递归:
console.time('add 加载时间')
const add = (num) => {
if (num === 1) return 1
return num + add(num - 1)
}
console.timeEnd('add 加载时间')
console.time('add2加载时间')
const add2 = (num, sum) => {
if (num === 1) return sum + num
return add2(num - 1, sum + num)
}
console.timeEnd('add2加载时间')
console.log(add(5))
console.log(add2(5, 0))
如果你觉得这篇文章对你有用,可以看看作者封装的库xtt-utils,里面封装了非常实用的js方法。如果你也是vue开发者,那更好了,除了常用的api,还有大量的基于element-ui组件库二次封装的使用方法和自定义指令等,帮你提升开发效率。不定期更新,欢迎交流~