可直接CV的 "树" 操作

902 阅读3分钟

前言

日常业务前端中关于树的操作是需要得心应手的,如级联框、树型控件、表格树等。本篇文章整理常见的树操作,便于快速的CV。

数组转树形结构

常见于后端返回的JSON数据需要前端去转化为树型数据。

案例

[{
  id: 1,
  pid: 0,
  name: '节点1'
},{
  id: 2,
  pid: 1,
  name: '节点1-1'
},{
  id: 3,
  pid: 1,
  name: '节点1-2'
},{
  id: 4,
  pid: 0,
  name: '节点2'
},{
  id: 5,
  pid: 4,
  name: '节点2-1'
}]

实现

  1. 非递归实现
function arrayToTree(data) {
  let res = []
  if(!Array.isArray(data)) return res
  let treeMap = {}
  data.forEach(item => treeMap[item.id] = item)
  data.forEach(item => {
    let parent = treeMap[item.pid]
    if(parent) {
      parent.children = !parent.children ? [] : parent.children
      parent.children.push(item)
    } else {
      res.push(item)
    }
  })
  return res
}
  1. 递归实现
function arrayToTree(data, pid) {
  let res = []
  if(!Array.isArray(data)) return res
  data.forEach(item => {
    if(item.pid === pid) {
      let children = arrayToTree(data, item.id)
      children.length && (item.children = children)
      res.push(item)
    }
  })
  return res
}                               

树形结构转数组

案例

[
  {
    id: 1,
    pid: 0,
    name: '节点1',
    children: [{
      id: 2,
      pid: 1,
      name: '节点1-1'
    },{
      id: 3,
      pid: 1,
      name: '节点1-2'
    }]
  },
  {
    id: 4,
    pid: 0,
    name: '节点2',
    children: [{
      id: 5,
      pid: 4,
      name: '节点2-1'
    }]
  }
]

实现

  1. 广度遍历
function treeToArr(data) {
  let res = []
  while(data.length) {
    let length = data.length
    for(let i = 0; i < length; i++) {
      let item = data.shift()
      if(item.children && item.children.length) {
      	for(let j = 0; j < item.children.length; j++) {
      	  data.push(item.children[j])
      	}
      } 
      res.push({
      	id: item.id,
      	pid: item.pid,
      	name: item.name
      })
    }
  }
  return res
}
  1. 深度遍历
function TreeToArr(data) {
  let res = []
  let stack = []
  for(let i = data.length - 1; i >= 0; i--) {
    stack.push(data[i])
  }
  while(stack.length) {
    let item = stack.pop()
    res.push({
      id: item.id,
      pid: item.pid,
      name: item.name
    })
    let children = item.children
    if(children && children.length) {
      for(let i = children.length - 1; i >= 0; i--) {
      	stack.push(children[i])
      }
    }
  }
  return res
}

树的高度

案例

const data = [{
  id: 1,
  children: [{
    id: 2,
    children: [{
      id: 3,
      children: []
    }]	
  }, 
  {
    id: 4,
    children: []	
  }]
}]

实现

  1. 广度遍历
function getTreeHeight(data) {
  let queue = []
  let height = 0
  if(!Array.isArray(data)) return height
  if(Array.isArray(data) && data.length > 0) queue[0] = data[0]
  
  while(queue.length) {
    let length = queue.length
    height++
    for(let i = 0; i < length; i++) {
      let item = queue.shift()
      if(item && item.children) {    	
      	for(let j = 0; j < item.children.length; j++) {
      	  item.children[j] && queue.push(item.children[j])
      	}
      }
    }
  }
  return height
}
  1. 深度遍历 深度遍历获取所有路径,再遍历路径数组获取最长路径即最大高度。
function getTreeHeight(data) {
  const paths = []
  let height = 0
  function dfs(data, prefix) {
    for(let i = 0; i < data.length; i++) {
      if(data[i].children && data[i].children.length) {
        const path = prefix  + data[i].id + ','
        dfs(data[i].children, path)
      } else {
        paths.push(prefix + data[i].id)
      }
    }
  }
  dfs(data, "")
  for(let i = 0; i < paths.length; i++) {
    let pathArr =  paths[i].split(',')
    height = Math.max(height, pathArr.length)
  }
  return height
}

查找节点在树中的路径

案例

[
  {
    id: 1,
    pid: 0,
    name: '节点1',
    children: [{
      id: 2,
      pid: 1,
      name: '节点1-1'
    },{
      id: 3,
      pid: 1,
      name: '节点1-2'
    }]
  },
  {
    id: 4,
    pid: 0,
    name: '节点2',
    children: [{
      id: 5,
      pid: 4,
      name: '节点2-1'
    }]
  }
]

实现

function getNodePath(data, id) {
  if(!Array.isArray(data)) return []
  let path = []
  function findTreeNode(tree, id) {
    for(let i = 0; i < tree.length; i++) {
      path.push(tree[i].id)
      if(tree[i].id === id) return path
      
      let children = tree[i].children
      if(children && children.length) {
        let findChildrenPath = findTreeNode(children, id)
        if(findChildrenPath && findChildrenPath.length) return findChildrenPath  
      }
      path.pop()
    }
    return []
  }
  return findTreeNode(data, id)
}

查找/编辑/删除节点

查找、编辑、删除操作都是基于遍历寻找对应 id,再去执行相关操作,这里以查找为例。

案例

[
  {
    id: 1,
    pid: 0,
    name: '节点1',
    children: [{
      id: 2,
      pid: 1,
      name: '节点1-1'
    },{
      id: 3,
      pid: 1,
      name: '节点1-2'
    }]
  },
  {
    id: 4,
    pid: 0,
    name: '节点2',
    children: [{
      id: 5,
      pid: 4,
      name: '节点2-1'
    }]
  }
]

实现

深度优先遍历

function findTreeNode(tree, id) {
  for(let i = 0; i < tree.length; i++) {
    let node = tree[i]
    if(node.id === id) {
      return node
    }
    let children = node.children
    if(children && children.length) {
      let childrenNode = findTreeNode(children, id)
      if(childrenNode) return childrenNode
    }
  }
  return
}

获取所有的叶子节点

案例

[
  {
    id: 1,
    pid: 0,
    name: '节点1',
    children: [{
      id: 2,
      pid: 1,
      name: '节点1-1'
    },{
      id: 3,
      pid: 1,
      name: '节点1-2'
    }]
  },
  {
    id: 4,
    pid: 0,
    name: '节点2',
    children: [{
      id: 5,
      pid: 4,
      name: '节点2-1'
    }]
  }
]

实现

function getLeafNode(data) {
  let res = []
  function dfs(tree) {
    for(let i = 0; i < tree.length; i++) {
      let children = tree[i].children
      if(!children || children.length === 0) {
        res.push({
          id: tree[i].id,
          pid: tree[i].pid,
          name: tree[i].name
        })
      } else {
        dfs(children)
      }
    }
  }
  dfs(data)
  return res
}