前端面试算法篇——二叉树的非递归遍历

1,386 阅读3分钟

前言

二叉树的宽度优先遍历(BFS)和深度优先遍历(DFS)(前序、中序、后序)是与树相关的基础知识,也是在这里很多人第一次接触到递归的思想。然而当掌握了递归后,实现树的深度优先遍历变得非常简单,因此笔者在最近的几次前端面试中被问及树的DFS,都被要求使用非递归的方法解决。所以想在这里做一些总结和记录,以方便自己和他人。 另外要说明的是,下面这棵树是本文的例子,以下所讨论的输出结果均以这棵树为例。

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

let root = new Node(1)
let node2 = new Node(2)
let node3 = new Node(3)
let node4 = new Node(4)
let node5 = new Node(5)
let node6 = new Node(6)
let node7 = new Node(7)
root.left = node2
root.right = node3
node2.left = node4
node2.right = node5
node4.left = node6
node4.right = node7

二叉树的宽度优先遍历

一般的BFS

function bfs(root){
  let queue = [root]
  let res = []
  while(queue.length){
    let shifted_node = queue.shift() //队首结点出队
    res.push(shifted_node.val)
    if(shifted_node.left){ //出队节点的左右子节点(如果有的话)入队
      queue.push(shifted_node.left)
    }
    if(shifted_node.right){
      queue.push(shifted_node.right)
    }
  }
  return res //[1,2,3,4,5,6,7]
}  

使用队列这一数据结构来实现二叉树的宽度优先遍历,先让根结点出队并记录下来,再让它的左右子节点入队,利用队列先进先出(FIFO)的特性,当队列为空时,所有的节点值都以BFS的方式记录了下来。队列是实现BFS的重要方法。

分层的BFS

function bfs_level(root){
  if(!root){
    return []
  }
  let res = []
  let cur = [root]  
  while(cur.length){
    let next = []
    let this_level = []
    for(let node of cur){  // 获得当前层的vals,并从当前层获得下一层的nodes
      this_level.push(node.val)
      if(node.left){
        next.push(node.left)
      }
      if(node.right){
        next.push(node.right)
      }
    }
    res.push(this_level)
    cur = next //更新cur为下一层
  }
  return res  //[[1], [2,3], [4,5], [6,7]]
}

分层的BFS能够记录每一层的节点,并且能指出哪一层有哪些节点,这在解决一些问题时是非常有用的。

二叉树的深度优先遍历

使用递归的DFS

正如前言所说,使用递归的DFS过于简单直接,在此不做赘述,只贴一段递归实现先序遍历的代码,中序与后序只需要改变result.push(root.val)的位置即可。

function preorderTraverse(root){
  let res = []
  pre_helper(root,res)
  return res
}

function pre_helper(root,result){
  if(!root){
    return 
  }
  result.push(root.val)
  pre_helper(root.left,result)
  pre_helper(root.right,result)
}

非递归实现前序遍历

function pre_norecur(root){
  if(!root){
    return []
  }
  let stack = [root]
  let res = []
  while(stack.length){
    let poped_node = stack.pop()
    if(poped_node.right){ //注意这里先压栈的是右子
      stack.push(poped_node.right)
    }
    if(poped_node.left){
      stack.push(poped_node.left)
    }
    res.push(poped_node.val)
  }
  return res  //[1,2,4,6,7,5,3]
}

非递归的先序遍历实际上思路和之前提到的一般的BFS相同,只不过在这里使用的是栈这一数据结构,因为栈后进先出(LIFO)的特点是非递归实现DFS的重要方法。在这里需要注意的是在将节点的左右子节点压栈时,先压入右节点。原因是先序遍历是根左右的顺序,而栈是后进先出,这样就很好的解释了为什么右子节点先压栈。

非递归实现后序遍历

function post_norecur(root){
  if(!root){
    return []
  }
  let stack1 = [root]
  let stack2 = []
  let res = []
  while(stack1.length){
    let poped_node = stack1.pop()
    if(poped_node.left){ //注意这里先压栈的是左子
      stack1.push(poped_node.left)
    }
    if(poped_node.left){
      stack1.push(poped_node.right)
    }
    stack2.push(poped_node.val) //先压入中间栈,因为是后序结果的倒序
  }
  while(stack2.length){
    res.push(stack2.pop())
  }
  return res //[6,7,4,5,2,3,1]
}

先说后序遍历的原因是,它的实现思路和前序遍历其实是一样的,如果你尝试的话会发现如果在压栈时先压入左子,后压入右子的话,会得到一个后序遍历的逆序,因此我们唯一需要做的就是使用一个中间栈,把这个逆序先存下来,然后把这个中间栈倾倒到res里使逆序变正。

非递归实现中序遍历

function in_nocur(root){
  let res = []
  let stack = []
  let cur = root
  while(stack.length || cur){
    while(cur){
      stack.push(cur) //把所有的靠左的点压栈
      cur = cur.left
    }
    cur = stack.pop() 
    res.push(cur.val)
    cur = cur.right
  }
  return res
}

先把所有的靠左的节点(左子的左子的左子...)压栈,然后pop出节点值存入res中,看它是否有右子,如果有则开始对右子树进行后序遍历,如果没有,则在这轮循环的最后cur会被赋值为null,在下一轮会继续被赋值为栈pop出的节点。