JavaScript 二叉树遍历专题:算法描述与实现

JavaScript 二叉树遍历专题:算法描述与实现

前端工程师在一般工作中用到算法的机会虽然不多,但是掌握基本的数据结构只是还是非常有必要的,在面试中,面试官也经常会选择一些简单的算法来考察候选人的编码能力,二叉树遍历就是出现率非常高的笔试题

不管是深度优先遍历还是广度优先遍历,都可以用递归算法或者非递归算法来实现,一般情况下递归算法比较容易实现,并且看起来直观,但是由于涉及到层层的函数栈调用,性能不高,而非递归算法有着比较好的性能。文章下面对深度优先遍历和广度优先遍历都做了算法描述和递归/非递归实现

首先我们定义这样的一个二叉树结构:

var nodes = {
  node: 6,
  left: {
    node: 5, 
    left: { 
      node: 4 
    }, 
    right: { 
      node: 3 
    }
  },
  right: { 
    node: 2, 
    right: { 
      node: 1 
    } 
  }
}

下面所有的代码都将对上述数据结构进行遍历。

深度优先遍历二叉树。

  1. 先序遍历(DLR)的算法:

递归算法

  1. 若二叉树为空,则算法结束,否则:
  2. 访问根结点;
  3. 前序遍历根结点的左子树;
  4. 前序遍历根结点的右子树。
var result = []
var dfs = function(nodes) {
  if(nodes.node) {
    result.push(nodes.node)
    nodes.left && dfs(nodes.left)
    nodes.right && dfs(nodes.right)
  }
}
dfs(nodes)
console.log(result)
// [6, 5, 4, 3, 2, 1]

非递归算法

  1. 初始化一个栈,将根节点压入栈中;
  2. 当栈为非空时,循环执行步骤3到4,否则执行结束;
  3. 出队列取得一个结点,访问该结点;
  4. 若该结点的右子树为非空,则将该结点的右子树入栈,若该结点的左子树为非空,则将该结点的左子树入栈;
var dfs = function(nodes) {
  var result = []
  var stack = []
  stack.push(nodes)
  while (stack.length) {
    var item = stack.pop()
    result.push(item.node)
    item.right && stack.push(item.right)
    item.left && stack.push(item.left)
  }
  return result
}
console.log(dfs(nodes))
// [6, 5, 4, 3, 2, 1]
  1. 中序遍历(LDR)的算法:

递归算法

  1. 若二叉树为空,则算法结束;否则:
  2. 中序遍历根结点的左子树;
  3. 访问根结点;
  4. 中序遍历根结点的右子树;
var result = []
var dfs = function(nodes) {
  if(nodes.node) {
    nodes.left && dfs(nodes.left)
    result.push(nodes.node)
    nodes.right && dfs(nodes.right)
  }
}
dfs(nodes)
console.log(result)
// [4, 5, 3, 6, 2, 1]

非递归算法

  1. 初始化一个栈,将根节点压入栈中,并标记为当前节点(item);
  2. 当栈为非空时,执行步骤3,否则执行结束;
  3. 如果当前节点(item)有左子树且没有被 touched,则执行4,否则执行5;
  4. 对当前节点(item)标记 touched,将当前节点的左子树赋值给当前节点(item=item.left) 并将当前节点(item)压入栈中,回到3;
  5. 清理当前节点(item)的 touched 标记,取出栈中的一个节点标记为当前节点(item),并访问,若当前节点(item)的右子树为非空,则将该结点的右子树入栈,回到3;
var dfs = function(nodes) {
  var result = []
  var stack = []
  var item = nodes
  stack.push(nodes)
  while (stack.length) {
    if(item.left && !item.touched) {
      item.touched = true
      item = item.left
      stack.push(item)
      continue
    }
    item.touched && delete item.touched // 清理标记
    item = stack.pop()
    result.push(item.node)
    item.right && stack.push(item.right)
  }
  return result
}
console.log(dfs(nodes))
// [4, 5, 3, 6, 2, 1]

  1. 后序遍历(LRD)的算法:

递归算法:

  1. 若二叉树为空,则算法结束,否则:
  2. 后序遍历根结点的左子树;
  3. 后序遍历根结点的右子树;
  4. 访问根结点。
var result = []
var dfs = function(nodes) {
  if(nodes.node) {
    nodes.left && dfs(nodes.left)
    nodes.right && dfs(nodes.right)
    result.push(nodes.node)
  }
}
dfs(nodes)
console.log(result)
// [4, 3, 5, 1, 2, 6]

非递归算法:

  1. 初始化一个栈,将根节点压入栈中,并标记为当前节点(item);
  2. 当栈为非空时,执行步骤3,否则执行结束;
  3. 如果当前节点(item)有左子树且没有被 touched,则执行4,如果被 touched left 但没有被 touched right 则执行5 否则执行6;
  4. 对当前节点(item)标记 touched left,将当前节点的左子树赋值给当前节点(item=item.left) 并将当前节点(item)压入栈中,回到3;
  5. 对当前节点(item)标记 touched right,将当前节点的右子树赋值给当前节点(item=item.right) 并将当前节点(item)压入栈中,回到3;
  6. 清理当前节点(item)的 touched 标记,弹出栈中的一个节点并访问,然后再将栈顶节点标记为当前节点(item),回到3;
var dfs = function(nodes) {
  var result = []
  var stack = []
  var item = nodes
  stack.push(nodes)
  while (stack.length) {
    if(item.left && !item.touched) {
      item.touched = 'left'
      item = item.left
      stack.push(item)
      continue
    }
    if(item.right && item.touched !== 'right') {
      item.touched = 'right'
      item = item.right
      stack.push(item)
      continue
    }
    var out = stack.pop()
    out.touched && delete out.touched // 清理标记
    result.push(out.node)
    item = stack.length ? stack[stack.length - 1] : null
  }
  return result
}
console.log(dfs(nodes))
// [4, 3, 5, 1, 2, 6]

广度优先遍历二叉树:

广度优先遍历二叉树(层序遍历)是用队列来实现的,从二叉树的第一层(根结点)开始,自上至下逐层遍历;在同一层中,按照从左到右的顺序对结点逐一访问。

按照从根结点至叶结点、从左子树至右子树的次序访问二叉树的结点。步骤:

  1. 初始化一个队列,并把根结点入列队;
  2. 当队列为非空时,循环执行步骤3到4,否则执行结束;
  3. 出队列取得一个结点,访问该结点;
  4. 若该结点的左子树为非空,则将该结点的左子树入队列,若该结点的右子树为非空,则将该结点的右子树入队列;

递归算法:

var result = []
var queue = [nodes]
var bfs = function(count) {
  count = count || 0
  if(queue[count]) {
    result.push(queue[count].node)
    var left = queue[count].left
    var right = queue[count].right
    if(left) {
      queue.push(left)
    }
    if(right) {
      queue.push(right)
    }
    bfs(++count)
  }
}
bfs()
console.log(result)
// [6, 5, 2, 4, 3, 1]

非递归算法

var bfs = function(nodes) {
  var result = []
  var queue = []
  var pointer = 0
  queue.push(nodes)
  while(pointer < queue.length) {
    var item = queue[pointer++] // 这里不使用 shift 方法(复杂度高),用一个指针代替
    result.push(item.node)
    console.log(item.node)
    item.left && queue.push(item.left)
    item.right && queue.push(item.right)
  }
  return result
}
console.log(bfs(nodes))
// [6, 5, 2, 4, 3, 1]
编辑于 2017-06-08