代码量超过了文字,慎读前端
前面部分是流水帐,不感兴趣的同窗能够直接略过😄node
记得刚入行前端的时候,我很是排斥使用框架,全部的功能,都要本身用原生写,那时候好有激情啊......,固然也是到处碰壁,其中某个管理系统中的一个树形表格折磨了我好久,后端的同窗直接甩给我一个相似这样的结构:面试
const list = [
{ id: 3, name: 'cc', parentId: 1, sort: 2 },
{ id: 4, name: 'dd', parentId: 1, sort: 1 },
{ id: 5, name: 'ee', parentId: 2, sort: 4 },
{ id: 6, name: 'ff', parentId: 3, sort: 3 },
{ id: 7, name: 'gg', parentId: 2, sort: 0 },
{ id: 8, name: 'hh', parentId: 4, sort: 1 },
{ id: 9, name: 'ii', parentId: 1, sort: 3 },
{ id: 1, name: 'aa', parentId: 0, sort: 1 },
{ id: 2, name: 'bb', parentId: 0, sort: 0 },
]
复制代码
而后需求大概是这个样子算法
要达到这种效果,首先得将这个列表转换成树形结构,而后遍历这个树形结构生成表格,过程当中还须要算出子节点的深度等等后端
当时思考良久而答案始终不可得🤔,因而百度一番首次接触了递归(竟然还能搜到😊):数组
function fn(data, pid) {
var result = [], temp;
for (var i = 0; i < data.length; i++) {
if (data[i].pid == pid) {
var obj = {"text": data[i].name,"id": data[i].id};
temp = fn(data, data[i].id);
if (temp.length > 0) {
obj.children = temp;
}
result.push(obj);
}
}
return result;
}
复制代码
而后我根据个人状况将上面的写法改造了一番终于将功能实现了,当时实现完后是满满的成就感呐!框架
不久以后,我回头再看这个转换,能不能不使用递归进行转换呢?当时了解递归时也知道递归的性能不怎么样,而且后端甩给个人列表也有些大,我测试每次转换大概要消耗大几十毫秒,因此这成了我内心的一道坎,得翻过去啊,因而🤔函数
function toTree (tree, topId) {
tree.forEach(function (child) {
child.children = tree.filter(function (cd) {
return cd.parentId === child.id
})
})
return tree.filter(child => child.parentId === topId)
}
复制代码
太爽了,当时看到这样的代码,起码得兴奋一夜,况且仍是我本身想出来的😄。上面的原理就是,利用数组是一维的,同时利用引用类型的特性,直接改变节点的属性值,而后根据topId过滤一下就能获得转换好的树。这很好,避开了递归,而后我将先后2种方式比较了一下,效率差很少提高20几倍左右(应该没记错,几年了)。性能
又过了不久,印证了一句话,too young too simple,我偶然从某篇文章(不记得了)中了解到了深度优先和广度优先、时间复杂度和空间复杂度这几个词,因而开启了新世界。测试
上面的作法,看似简洁,但在时间复杂度上的表现彷佛太弱了,每一次循环都要进行一次filter操做,返回时又是一次循环,这是不必的。
接下来我将以往的一些相关的经验总结分享一下......,
继承上面的一丢丢思路,通过个人改良版本,原理仍是利用了引用类型的特性,用一个map经过节点的id
保存每个节点的children
引用,而后一边遍历一边更新children
,最后清空map。
function convertListToTree (list, topId = 0) {
let result
let map = {}
list.forEach(child => {
const { id, parentId } = child
const children = map[parentId] = map[parentId] || []
if (!result && parentId === topId) {
result = children
}
children.push(child)
child.children = map[id] || (map[id] = [])
})
map = null
return result
}
复制代码
上面的代码量多了一些,但仔细看会发现,整个转换过程只经历了一次循环O(n)
,相比以前的方法,优化了不少倍。但仅仅是转换彷佛不太够,可能有时候除了转换还须要排序等其它操做。以排序来讲,若是能尽可能减小时间复杂度,那确定最好不过了,这里能够在拿到全部children以后直接利用map来完成,遍历map,而后对每一个children数组sort操做,如今将代码再改造一下
function convertListToTree(list, callBack = c => c, options = {}) {
const {
topId = 0,
idKey = 'id',
parentIdKey = 'parentId',
sortKey
} = options
let result
let map = {}
list.forEach(child => {
const { [idKey]: id, [parentIdKey]: parentId } = child
const children = map[parentId] = map[parentId] || []
if (!result && parentId === topId) {
result = children
}
children.push( callBack(child) || child )
child.children = map[id] || (map[id] = [])
})
sortKey && Object.keys(map).forEach(i => {
if (map[i].length > 1) {
map[i].sort((a, b) => a[sortKey] - b[sortKey])
}
})
map = null
return result
}
复制代码
如今对最开始的list
进行转换
const tree = convertListToTree(list, node => {
node.someKey = 1
}, { sortKey: 'sort' })
复制代码
搞定了列表转树,但若是遍历树呢?
深度优先搜索算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。沿着树的深度遍历树的节点,尽量深的搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。 这一过程一直进行到已发现从源节点可达的全部节点为止。-----维基百科
按照深度优先搜索,上图的遍历顺序为: a b e f c d g h
广度优先搜索算法(英语:Breadth-First-Search,缩写为BFS),又译做宽度优先搜索,或横向优先搜索,是一种图形搜索算法。简单的说,BFS是从根节点开始,沿着树的宽度遍历树的节点。若是全部节点均被访问,则算法停止。-----维基百科
按照广度优先搜索,上图的遍历顺序为: a b c d e f g h
都很容易理解,接下来,先将他们简单实现
深度优先递归方式 原理就是每碰到有children的节点递归调用一下
function recursiveEachByDfs (tree, cb) {
let i = 0
let node
while (node = tree[i++]) {
cb(node)
if (node.children && node.children.length) {
recursiveEachByDfs(node.children, cb)
}
}
}
复制代码
深度优先非递归方式 原理就是利用栈,后进先出,每次碰到children,就将children压入栈顶
function eachByDfs([...stack], cb) {
while (stack.length) {
// 出栈
const node = stack.shift()
cb(node)
if (node.children && node.children.length) {
// 入栈
stack.unshift(...node.children)
}
}
}
复制代码
广度优先递归方式 原理就是,遍历当前层的节点时,将遇到children存入数组中,做为tree传入下一个调用,终止条件为没有下一层节点时
function recursiveEachByBfs(tree, cb) {
const nextLevels = []
// 遍历当前层同时拿到下一层节点
let i = 0
let node
while (node = tree[i++]) {
cb(node)
nextLevels.push(...node.children)
}
// 递归下一层全部节点
if (nextLevels.length) {
recursiveEachByBfs(nextLevels, cb)
}
}
复制代码
广度优先非递归方式 原理就是利用队列,先进先出,让碰到children去排队,就像食堂排队打饭同样
function eachByBfs([...queue], cb) {
while (queue.length) {
// 出队
const node = queue.shift()
cb(node)
if (node.children && node.children.length) {
// 入队
queue.push(...node.children)
}
}
}
复制代码
如今我们就能经过上面任意一种方式遍历树形结构了,但仅仅是这样确定是不够的。根据我以往碰到的需求,至少还可能须要
首先,我们先大概分析一下上述功能:
continue
仍是break
那接下来,咱们将上面的4个基本方法都改造一下,所有支持上述功能,而后供君挑选。
relations[0]
: 节点深度relations[1]
: 全部父节点,按深度顺序排列只须要通过函数参数传递一下
function recursiveEachByDfs(tree, cb, relations = [0, []]) {
let i = 0
let node
while (node = tree[i++]) {
const behavior = cb(node, ...relations)
if (behavior === 'break') {
break
}
if (behavior === 'continue') {
continue
}
if (node.children) {
const [level, parents] = relations
recursiveEachByDfs(node.children, cb, [level + 1, parents.concat(node)])
}
}
}
// 搜索根节点id为0的分支,最深搜索3层
recursiveEachByDfs(tree, (node, level, parents) => {
if (level > 2) {
return 'continue'
}
if (level === 0 && node.id !== 0) {
return 'continue'
}
console.log(node, level, parents)
})
复制代码
既然用到了栈,那就能够往栈中加点料,原理就是在每一次入栈时,将入栈子节点们的父节点和深度信息(就叫信息对象吧,不限于这些东西)等一块儿放到栈顶,以后在每次出栈时判断若是不是节点类型的值,就说明这是接下来遍历的子节点们的父节点以及深度信息,用一张图来描述(基于本章第二张图):
function eachByDfs([...stack], cb) {
let relations = [0, []]
// 将信息对象压入栈顶
stack.unshift(relations)
while (stack.length) {
let node = stack.shift()
if (Array.isArray(node)) {
relations = node
node = stack.shift()
}
const behavior = cb(node, ...relations)
if (behavior === 'break') {
break
}
if (behavior === 'continue') {
continue
}
if (node.children && node.children.length) {
// 若是此时栈顶是信息对象,说明当前节点是它的父节点中子节点的最后一个了
if (stack.length && !Array.isArray(stack[0])) {
stack.unshift(relations)
}
const [level, parents] = relations
stack.unshift([level + 1, parents.concat(node)], ...node.children)
}
}
}
// 搜索根节点id为0的分支,最深搜索3层
eachByDfs(tree, (node, level, parents) => {
if (level > 2) {
return 'continue'
}
if (level === 0 && node.id !== 0) {
return 'continue'
}
console.log(node, level, parents)
})
复制代码
首次进入函数时,初始化一个信息对象,以后,将每一层的节点被统一放到数组中(将问题统一,就像处理第一层节点同样),除了第一层节点的其它层的节点可能有不一样的父节点,因此须要像插队同样,在每一个节点的children以前插入一个信息对象
function recursiveEachByBfs(tree, cb) {
const nextLevels = []
let relations = [0, []]
if (!Array.isArray(tree[0])) {
// 首次进入,放入父节点和深度信息
tree.unshift(relations)
}
let i = 0
let node
while (node = tree[i++]) {
if (Array.isArray(node)) {
relations = node
continue
}
const behavior = cb(node, ...relations)
if (behavior === 'break') {
break
}
if (behavior === 'continue') {
continue
}
const [level, parents] = relations
// 在每一个节点的子节点以前插入信息对象
nextLevels.push([level + 1, parents.concat(node)], ...node.children)
}
// 递归下一层全部节点
if (nextLevels.length) {
recursiveEachByBfs(nextLevels, cb)
}
}
// 搜索根节点id为0的分支,最深搜索3层
recursiveEachByBfs(tree, (node, level, parents) => {
if (level > 2) {
return 'continue'
}
if (level === 0 && node.id !== 0) {
return 'continue'
}
console.log(node, level, parents)
})
复制代码
思路和深度优先非递归相似,只不过这里换成来队列,但处理起来比栈稍微简单点,在每一个节点的children以前插入一个信息对象......
function eachByBfs([...queue], cb) {
let relations = [0, []]
queue.unshift(relations)
while (queue.length) {
let node = queue.shift()
if (Array.isArray(node)) {
relations = node
node = queue.shift()
}
const behavior = cb(node, ...relations)
if (behavior === 'break') {
break
}
if (behavior === 'continue') {
continue
}
if (node.children && node.children.length) {
const [level, parents] = relations
queue.push([level + 1, parents.concat(node)], ...node.children)
}
}
}
// 搜索根节点id为0的分支,最深搜索3层
eachByBfs(tree, (node, level, parents) => {
if (level > 2) {
return 'continue'
}
if (level === 0 && node.id !== 0) {
return 'continue'
}
console.log(node, level, parents)
})
复制代码
如今,基本的功能都实现了,主要是以往作的项目都过小了,至今没发现什么更复杂的场景,若是有,那就继续改造......,思路都差很少
这个我只在刷面试题的时候碰到......,这里简单实现一下,思路都是同样的,知道怎么遍历就不是问题了😄,仅供参考,统一的思路就是将对象的每一个属性拆解成一个信息对象,对象中包含key、value、parent等等等等,而后和处理数组的方式差很少,有以下粒子
const symbolName = Symbol()
const obj = {
a: {
b: {
c: 1
},
d: [{ j: 1 }, { [symbolName]: 222 }]
},
e: {
f: 3
},
g: {
h: {
i: 4
}
}
}
复制代码
深度优先递归
const recursiveEachObjByDfs = (obj, cb) => {
Reflect.ownKeys(obj).forEach(key => {
const value = obj[key]
cb({ key, value, parent: obj })
if (typeof value === 'object') {
recursiveEachObjByDfs(value, cb)
}
})
}
recursiveEachObjByDfs(obj, console.log)
复制代码
深度优先非递归
const eachObjByDfs = (obj, cb) => {
const stack = Reflect.ownKeys(obj).map(key => ({key, value: obj[key], parent: obj}))
while (stack.length) {
const options = stack.shift()
cb(options)
if (typeof options.value === 'object') {
stack.unshift(...Reflect.ownKeys(options.value).map(key => {
return {
key,
value: options.value[key],
parent: options.value
}
}))
}
}
}
eachObjByDfs(obj, console.log)
复制代码
广度优先递归
const recursiveEachObjByBfs = (obj, cb) => {
const currentLevels = Reflect.ownKeys(obj).map(key => ({key, value: obj[key], parent: obj}))
const each = levels => {
const nextLevels = []
let i = 0
let options
while (options = levels[i++]) {
cb(options)
if (typeof options.value === 'object') {
nextLevels.push(...Reflect.ownKeys(options.value).map(key => {
return {
key,
value: options.value[key],
parent: options.value
}
}))
}
}
if (nextLevels.length) {
each(nextLevels)
}
}
each(currentLevels)
}
recursiveEachObjByBfs(obj, console.log)
复制代码
广度优先非递归
const eachObjByBfs = (obj, cb) => {
const queue = Reflect.ownKeys(obj).map(key => ({key, value: obj[key], parent: obj}))
while (queue.length) {
const options = queue.shift()
cb(options)
if (typeof options.value === 'object') {
queue.push(...Reflect.ownKeys(options.value).map(key => {
return {
key,
value: options.value[key],
parent: options.value
}
}))
}
}
}
eachObjByBfs(obj, console.log)
复制代码
写得很差,莫计较,献给须要的同窗,若是文章中有什么错误或者有什么能够改进的地方,能够下方留言咯。