Skip to content

树形结构操作合集

javascript
let 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: [{ id: '2-1-1', title: '节点2-1-1', parentId: '2-1' }],
      },
    ],
  },
]
let list = [
  { id: '1', title: '节点1', parentId: '' },
  { id: '1-1', title: '节点1-1', parentId: '1' },
  { id: '1-2', title: '节点1-2', parentId: '1' },
  { id: '2', title: '节点2', parentId: '' },
  { id: '2-1', title: '节点2-1', parentId: '2' },
  { id: '2-1-1', title: '节点2-1-1', parentId: '2-1' },
]

一、遍历

1. 广度优先

javascript
/** 循环实现
 *	思路:维护一个队列,队列的初始值为树结构根节点组成的列表,重复执行以下步骤直到队列为空:
 *	取出队列中的第一个元素,进行访问相关操作,然后将其后代元素(如果有)全部追加到队列最后。
 */
const forEachTree = (tree, fn) => {
  let node,
    data = [...tree]
  while ((node = data.shift())) {
    fn(node)
    node.children && data.push(...node.children)
  }
}
var res = forEachTree(tree, (node) => console.log(node.id))
// 输出:
1
2
1 - 1
1 - 2
2 - 1
2 - 1 - 1

2. 深度优先

  • 先序遍历
javascript
// 递归实现
const forEachTree = (tree, fn) => {
  tree.forEach((item) => {
    fn(item)
    item.children && forEachTree(item.children, fn)
  })
}

/**
 * 循环实现
 * 与广度优先类似,只是将自己unshift到数组前面。
 */
const forEachTree = (tree, fn) => {
  let node,
    data = [...tree]
  while ((node = data.shift())) {
    fn(node)
    node.children && data.unshift(...node.children)
  }
}

var res = forEachTree(tree, (node) => console.log(node.id))
// 输出:
1
1 - 1
1 - 2
2
2 - 1
2 - 1 - 1
  • 后序遍历
javascript
// 递归实现
const forEachTree = (tree, fn) => {
  tree.forEach((item) => {
    item.children && forEachTree(item.children, fn)
    fn(item)
  })
}
var res = forEachTree(tree, (node) => console.log(node.id))
// 输出
1 - 1
1 - 2
1
2 - 1 - 1
2 - 1
2

3. 返回新树

javascript
const forEachTree = (tree, newTree = []) => {
  tree.map((k, v) => {
    let node = {
      ...k,
    }
    if (k?.children?.length) {
      node.children = []
      forEachTree(k.children, node.children)
    }
    newTree.push(node)
  })
  return newTree
}

二、树与列表转换

1. 树转表

javascript
// 循环实现 利用(广度||深度)优先遍历,为子级添加level以及parentId。
const treeTolist = tree => {
  let node, list = []
 	while(node = tree.shift()) {
    node.level = node.level || 0
    list.push(node)
    node.children && tree.push(...node.children.map(item => ({...item, parentId: node.id, level: node.level + 1})))
 }
 return list
}
// 循环实现2  思路与1并无二致,更加接地气罢了
const treeTolist = tree =>  {
  let  result = tree.map(node => (node.level = 0, node))
  for (let i = 0; i < result.length; i++) {
    if (!result[i].children) continue
    let list = result[i].children.map(node => (node.level = result[i].level + 1, node))
    result.splice(i+1, 0, ...list)
  }
  return result
}

// 递归实现
const treeTolist = (tree, result = [], level = 0) => {
  for (let node of tree) {
    node.level = level
    result.push(node)
    node.children && treeTolist(node.children.map(item => ({...item, parentId: node.id })), result, level + 1)
  }
  return result
}

var res = treeTolist(tree)
console.log(res, 'res')
// 输出
0: {id: "1", title: "节点1", children: Array(2), level: 0}
1: {id: "1-1", title: "节点1-1", parentId: "1", level: 1}
2: {id: "1-2", title: "节点1-2", parentId: "1", level: 1}
3: {id: "2", title: "节点2", children: Array(1), level: 0}
4: {id: "2-1", title: "节点2-1", children: Array(1), parentId: "2", level: 1}
5: {id: "2-1-1", title: "节点2-1-1", parentId: "2-1", level: 2}

2. 表转树

javascript
// 列表结构转为树结构,就是把所有非根节点放到对应父节点的chilren数组中,然后把根节点提取出来:
const listTotree = (list) => {
  const info = list.reduce((map, node) => ((map[node.id] = node), (node.children = []), map), {})
  return list.filter((node) => {
    node.parentId && info[node.parentId].children.push(node)
    return !node.parentId
  })
}

var res = listTotree(list)

//方法2 双重循环
const listTotree = (list) => {
  const newList = list.slice(0)
  newList.forEach((item) => {
    const parentId = item.parentId
    parentId &&
      newList.forEach((key) => {
        if (key.id === parentId) {
          key.children = key.children || []
          key.children.push(item)
        }
      })
  })
  return newList.filter((item) => !item.parentId)
}

三、树结构查找

节点查找

  • 单节点
javascript
// 循环实现
const nodeSeek = (tree, fn) => {
  let node, data = [...tree]
  while(node = data.shift()){
  	if (fn(node)) return node
    node.children && data.unshift(...node.children)
  }
  return null
}

// 递归实现
const nodeSeek = (tree, fn) => {
  for (let node of tree){
  	if (fn(node)) return node
    if (node.children) {
    	const res = nodeSeek(node.children, fn)
      if (res) return res
    }
  }
  return null
}

// 递归实现2 借助扁平化
 const nodeSeek = (tree, fn) => {
   const flatten = arr => {
     //  eslint-disable-next-line
     return arr.reduce((res, next) => (res = res.concat(Array.isArray(next.children) ? flatten(next.children) : next), res), [])

     // return arr.reduce((res, next) => {
     //   return res.concat(Array.isArray(next.children) ? flatten(next.children) : next)
     // }, [])
   }
   return flatten(tree).find(fn)
 }

var res = nodeSeek(tree, (node) => node.id === '2-1')
console.log(res, 'res')

// 输出
{
  children: [{…}],
  id: "2-1",
  title: "节点2-1",
}
  • 多节点
javascript
const nodeSeek = (tree, fn, result = []) => {
  let node,
    data = [...tree]
  while ((node = data.shift())) {
    if (fn(node)) result.push(node)
    node.children && data.unshift(...node.children)
  }
  return result
}

var res = nodeSeek(tree, (node) => node.id === '2-1' || node.id === '1-1')
console.log(res, 'res')[
  // 输出:
  ({ id: '1-1', title: '节点1-1' }, { id: '2-1', title: '节点2-1', children: Array(1) })
]

单路径查找

javascript
const treePath = (tree, fn, path = []) => {
  for (let data of tree) {
    path.push(data.id)
    if (fn(data)) return path
    if (data.children) {
      const res = treePath(data.children, fn, path)
      if (res) return res
    }
    path.pop()
  }
  return null
}

let result = treePath(tree, (node) => node.id === '2-1-1')
console.log(result) // ["2", "2-1", "2-1-1"]

多路径查找

javascript
const treePath = (tree, fn, path = [], result = []) => {
  for (let node of tree) {
    path.push(node.id)
    fn(node) && result.push([...path])
    node.children && treePath(node.children, fn, path, result)
    path.pop()
  }
  return result
}
console.log(treePath(tree, (node) => node.id === '2-1-1' || node.id === '2-1'))[
  // 输出
  (['2', '2-1'], ['2', '2-1', '2-1-1'])
]

四、树结构裁剪

树结构过滤即保留某些符合条件的节点,剪裁掉其它节点。一个节点是否保留在过滤后的树结构中,取决于它以及后代节点中是否有符合条件的节点。可以传入一个函数描述符合条件的节点:

javascript
const treeFilter = (tree, func) =>{
  return tree.filter(node => {
    node.children = node.children && treeFilter(node.children, func)
    return func(node) || (node.children && node.children.length)
  })
}
var res = treeFilter(tree, (node) => node.id === '2-1')
console.log(res, 'res')
// 输出
[{
  id: "2"
  title: "节点2",
  children: [{
    id: "2-1"
    title: "节点2-1"
  }]
}]

五、关键字过滤

javascript
const filterTree = (val, tree, newArr = []) => {
  if (!(tree.length && val)) {
    // 如果搜索关键字为空直接返回源数据
    return tree
  }
  for (let item of tree) {
    if (item.title.indexOf(val) > -1) {
      // 匹配到关键字的逻辑
      newArr.push(item) // 如果匹配到就在数值中添加记录
      continue // 匹配到了就退出循环了此时如果有子集也会一并带着
    }
    if (item.children && item.children.length) {
      // 如果父级节点没有匹配到就看看是否有子集,然后做递归
      let subArr = filterTree(val, item.children) // 缓存递归后的子集数组
      if (subArr && subArr.length) {
        // 如果子集数据有匹配到的节点
        let node = { ...item, children: subArr } // 关键逻辑,缓存父节点同时将递归后的子节点作为新值
        newArr.push(node) // 添加进数组
      }
    }
  }
  return newArr
}

// https://www.jianshu.com/p/023e5212f53a

君子慎独