前言
二叉树的宽度优先遍历(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出的节点。