JS数据结构之树的遍历(Tree Traversal)

4,800 阅读3分钟

预备工作

先写几个函数,能够生成树状结构的数据

生成随机数据

// 支持string、number、boolean和object类型随机数据
const randomData = {
  get string() {
    return Math.random().toString(36).slice(2, 6)
  },
  get number() {
    return (Math.random() * 10000) | 0
  },
  get boolean() {
    return !((Math.random() * 2) | 0)
  },
  get object() {
    return {
      id: this.number,
      name: this.string,
    }
  },
}

randomData[类型] 调用即可。

生成随机多叉树

function randomTree(n, type = 'string') {
  let root = {}, arr = []
  for (let i = 0; i < n; i++) {
    const node = { element: randomData[type], children: [] }
    const parent = arr[(Math.random() * arr.length) | 0]
    parent ? parent.children.push(node) : (root = node)
    arr.push(node)
  }
  return root
}

生成随机二叉树

function randomBinaryTree(n, type = 'string') {
  let root = {}, arr = []
  for (let i = 0; i < n; i++) {
    const node = { element: randomData[type], left: null, right: null }
    let parent = arr[(Math.random() * arr.length) | 0]
    if (parent) {
      const child = (Math.random() * 2) | 0 ? 'left' : 'right'
      if (parent[child]) while ((parent = parent[child])[child]);
      parent[child] = node
    } else root = node
    arr.push(node)
  }
  return root
}

多叉树的遍历

节点结构

function Node(element) {
  this.element = element
  this.children = []
}

深度优先遍历

递归算法

递归实现的思路比较简单,就是先取根节点,然后对其子节点递归遍历即可:

function depthFirstTraversalRecursion(root, arr = []) {
  if (!root) return arr
  const { element, children } = root
  arr.push(element)
  children.forEach((child) => depthFirstTraversal(child, arr))
  return arr
}

非递归算法

非递归要用到栈这种数据结构,每个出栈的节点,要对其子节点按照后进先出的原则入栈:

function depthFirstTraversal(root, arr = []) {
  if (!root) return arr
  let stack = [root], current
  while ((current = stack.pop())) {
    const { element, children } = current
    arr.push(element)
    for (let i = children.length - 1; i >= 0; i--) stack.push(children[i])
  }
  return arr
}

广度优先遍历

也就是层序遍历,这里要用到队列这种数据结构,先进先出

function breadthFirstTraversal(root, arr = []) {
  if (!root) return
  let queue = [root], current
  while ((current = queue.shift())) {
    const { element, children } = current
    arr.push(element)
    queue.push(...children)
  }
  return arr
}

结果验证

使用上面写好的随机函数 randomTree(6) 生成 6 个节点的树:

{
  element: 'w8bs',
  children: [
    {
      element: 'dbdd',
      children: [ { element: 'yt4s', children: [] } ]
    },
    {
      element: 'tgi8',
      children: [ { element: 'gtf3', children: [] } ]
    },
    { element: 'ixli', children: [] }
  ]
}

运行上面的代码,输出结果如下,与预期一致:

深度优先遍历(递归) [ 'w8bs', 'dbdd', 'yt4s', 'tgi8', 'gtf3', 'ixli' ]
深度优先遍历 [ 'w8bs', 'dbdd', 'yt4s', 'tgi8', 'gtf3', 'ixli' ]
广度优先遍历 [ 'w8bs', 'dbdd', 'tgi8', 'ixli', 'yt4s', 'gtf3' ]

二叉树的遍历

节点结构

function Node(element) {
  this.element = element
  this.left = null
  this.right = null
}

前/中/后序遍历

递归算法

递归算法比较直观,前中后序遍历只要调整一下顺序即可:

function traversal(root, type, arr = []) {
  if (!root) return arr
  const { element, left, right } = root
  switch (type) {
    case 'preorder':
      arr.push(element)
      traversal(left, type, arr)
      traversal(right, type, arr)
      break
    case 'inorder':
      traversal(left, type, arr)
      arr.push(element)
      traversal(right, type, arr)
      break
    case 'postorder':
      traversal(left, type, arr)
      traversal(right, type, arr)
      arr.push(element)
      break
  }
  return arr
}

非递归算法

同样是用到了栈结构,操作元素多次出入栈,需要增加一个辅助属性 _visited 来区分是否曾经进过栈或访问过,每个节点,只有被第二次访问的时候才进行打印:

function traversal(root, type, arr = []) {
  if (!root) return arr
  let stack = [root], current
  while ((current = stack.pop())) {
    const { element, left, right, _visited } = current
    if (!_visited) {
      current._visited = true
      switch (type) {
        case 'preorder':
          if (right) stack.push(right)
          if (left) stack.push(left)
          stack.push(current)
          break
        case 'inorder':
          if (right) stack.push(right)
          stack.push(current)
          if (left) stack.push(left)
          break
        case 'postorder':
          stack.push(current)
          if (right) stack.push(right)
          if (left) stack.push(left)
          break
      }
    } else {
      arr.push(element)
      delete current._visited
    }
  }
  return arr
}

层序遍历

与多叉树的类似,也是使用了队列这种数据结构:

function levelOrderTraversal(root, arr = []) {
  if (!root) return arr
  let queue = [root], current
  while ((current = queue.shift())) {
    const { element, left, right } = current
    arr.push(element)
    if (left) queue.push(left)
    if (right) queue.push(right)
  }
  return arr
}

结果验证

使用上面写好的随机函数 randomBinaryTree(6) 生成 6 个节点的二叉树:

{
  element: 'a8kk',
  left: {
    element: '7n9z',
    left: {
      element: 'k0pb',
      left: { element: 'hxk0', left: null, right: null },
      right: null
    },
    right: null
  },
  right: {
    element: 'da0r',
    left: null,
    right: { element: 'mgr1', left: null, right: null }
  }
}

分别调用上面的递归与非递归遍历算法,得到的结果与预期一致:

前序遍历(递归) [ 'a8kk', '7n9z', 'k0pb', 'hxk0', 'da0r', 'mgr1' ]
中序遍历(递归) [ 'hxk0', 'k0pb', '7n9z', 'a8kk', 'da0r', 'mgr1' ]
后续遍历(递归) [ 'hxk0', 'k0pb', '7n9z', 'mgr1', 'da0r', 'a8kk' ]
前序遍历 [ 'a8kk', '7n9z', 'k0pb', 'hxk0', 'da0r', 'mgr1' ]
中序遍历 [ 'hxk0', 'k0pb', '7n9z', 'a8kk', 'da0r', 'mgr1' ]
后序遍历 [ 'hxk0', 'k0pb', '7n9z', 'mgr1', 'da0r', 'a8kk' ]