JS中树形结构的平常操做

代码量超过了文字,慎读前端

前面部分是流水帐,不感兴趣的同窗能够直接略过😄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)
    }
  }
}
复制代码

如今我们就能经过上面任意一种方式遍历树形结构了,但仅仅是这样确定是不够的。根据我以往碰到的需求,至少还可能须要

  1. 在遍历时计算出当前节点的深度
  2. 在遍历时计算出当前节点的全部父节点
  3. 更新当前节点的部分数据
  4. 根据id寻找节点
  5. 支持遍历某一个分支,或者知足某些条件的遍历

首先,我们先大概分析一下上述功能:

  • 第一、2点,能够当作是相似的需求,不论是深度优先仍是广度优先,都是从父到子搜索的过程,利用这一点,能够将当前节点信息(也便是以后节点的父节点)层层传递下去,而后每通过一层,层层叠加。
  • 第三、4点,这就是个回调方法传入当前节点搞定了,当查找到节点就不必继续循环了
  • 第5点,和第4点相似,能够在遍历时接收一下回调函数的返回值,经过返回值决定循环是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)
复制代码

写得很差,莫计较,献给须要的同窗,若是文章中有什么错误或者有什么能够改进的地方,能够下方留言咯。

相关文章
相关标签/搜索