基本概念
二叉树遍历主要为深度优先(DFS)和广度优先(BFS),其中深度优先遍历包括前序、中序、后序,广度优先遍历也叫层序遍历。
深度优先三种方法遍历顺序:
其实很好记,就是中间节点在最前面、中间和最后面输出,而左右的相对顺序是固定的。
例如下面这棵树:
1
/ \
2 3
/ \ / \
4 5 6 7
前序遍历:1, 2, 4, 5, 3, 6, 7
中序遍历:4, 2, 5, 1, 6, 3, 7
后序遍历:4, 5, 2, 6, 7, 3, 1
深度优先和广度优先都能通过递归实现,由于递归方法过于简单,面试考察的通常是非递归实现。深度优先的非递归方法通过堆栈实现,广度优先的非递归方法通过队列实现。
递归实现
前序遍历:
function preOrder(root) {
if (root == null)
return;
console.log(root.val); // 输出控制
preOrder(root.left);
preOrder(root.right);
}
而中序遍历和后序遍历则只需修改输出控制的位置。
中序遍历:
function inOrder(root) {
if (root == null)
return;
inOrder(root.left);
console.log(root.val); // 输出控制
inOrder(root.right);
}
后序遍历:
function postOrder(root) {
if (root == null)
return;
postOrder(root.left);
postOrder(root.right);
console.log(root.val); // 输出控制
}
非递归实现
非递归版本使用堆栈实现,三种遍历方法主要是压栈弹栈的顺序不同。
前序遍历:
一开始先把根节点压栈,每次弹出栈顶元素的同时输出该元素,然后把栈顶元素的右节点、左节点分别入栈(如果有的话,为空则不用);直到栈为空则停止。
代码如下:
function preOrderByStack(root) {
let res = new Array();
if (root == null) // 边界判断
return res;
let stack = new Array();
stack.push(root); // 先把根节点压栈
while (stack.length > 0) {
root = stack.pop(); // 弹出当前栈顶元素
res.push(root.val); // 保存结果
if (root.right != null) {
stack.push(root.right); // 先压入右节点
}
if (root.left != null) {
stack.push(root.left); // 再压入左节点
}
}
return res;
}
上面的代码复用了root
变量,好处就是不使用额外的变量,降低空间复杂度。
后序遍历:
后序遍历有一种非常trick
的做法。我们知道先序遍历为中左右
,而后序遍历为左右中
,我们把后序遍历反过来,就是中右左
,是不是发现和先序遍历有点像了?我们先序遍历采用了先压入右节点再压入左节点的方式得到了中左右
的顺序,那么我们只要先压入左节点,再压入右节点,就能得到中右左
的顺序,这里只要保存结果的时候从前往后插入,就变成了我们想要的后序遍历了:左右中
。
function preOrderByStack(root) {
let res = new Array();
if (root == null)
return res;
let stack = new Array();
stack.push(root);
while (stack.length > 0) {
root = stack.pop();
res.unshift(root.val); // 从数组头部添加结果
if (root.left != null) {
stack.push(root.left); // 先压入左节点
}
if (root.right != null) {
stack.push(root.right); // 再压入右节点
}
}
return res;
}
后序遍历可以求二叉树的深度。
中序遍历:
当前节点只要有左节点,就将其左节点压栈,并且当前节点向其左节点方向移动,直到当前节点为空,说明此时位于最左下方的节点的空左节点处,那么接下来我们就需要弹栈获取栈顶,输出元素,然后移动到栈顶节点的右节点处。
代码如下:
function inOrderByStack(root) {
let res = new Array();
let stack = new Array();
while (stack.length > 0 || (root != null)) {
if (root != null) { // 当前节点非空,压栈后向左移动
stack.push(root);
root = root.left;
} else { // 当前节点为空,弹栈输出后向右移动
root = stack.pop();
res.push(root.val);
root = root.right;
}
}
return res;
}
上面介绍的三种方法可以用于遍历,但是打印路径就不太方便。如果需要打印出二叉树的所有路径,可以使用下面的代码:
function dfsToPath(root) {
let res = new Array();
if(!root) return res;
let stack = new Array();
stack.push(root);
while(stack.length > 0) {
root = stack[stack.length-1]; // 获取栈顶元素但不弹出该元素
// 存在左节点且左节点未遍历
if(root.left && !root.isLeft) {
root.isLeft = true; // 标记为已遍历
stack.push(root.left);
continue;
}
// 存在右节点且右节点未遍历
if(root.right && !root.isRight) {
root.isRight = true; // 标记为已遍历
stack.push(root.right);
continue;
}
// 遍历到叶子节点时保存当前路径
if(!root.left && !root.right) {
let temp = stack.map(item => item.val);
res.push(temp);
}
// 弹出叶子节点或者子树都已遍历的节点
stack.pop();
}
return res
}
下面还有两个递归版本,也可以参考下。
下面的代码输出结果是形如[ '1->2->4', '1->2->5', '1->3->6', '1->3->7' ]
的字符串数组:
function binaryTreePathsString(root) {
const paths = [];
const construct_paths = (root, path) => {
if (root) {
path += root.val.toString();
if (root.left === null && root.right === null) { // 当前节点是叶子节点
paths.push(path); // 把路径加入到答案中
} else {
path += "->"; // 当前节点不是叶子节点,继续递归遍历
construct_paths(root.left, path);
construct_paths(root.right, path);
}
}
}
construct_paths(root, "");
return paths
}
下面代码输出的结果是形如[ [ 1, 2, 4 ], [ 1, 2, 5 ], [ 1, 3, 6 ], [ 1, 3, 7 ] ]
的数组:
function binaryTreePathsArray(root) {
let paths = [];
const resc = (root, path) => {
if(root) {
// path.push(root.val); 这样的写法是错误的
// 数组是引用类型,跟上面代码的字符串是不一样的
// 每次递归的时候都要浅拷贝一下
path = [...path, root.val];
if(root.left == null && root.right == null) {
paths.push(path);
} else {
resc(root.left, path);
resc(root.right, path);
}
}
}
resc(root, []);
console.log(paths);
}
掌握上面的代码,下面的问题应该都能解决了:
剑指 Offer 68 - II. 二叉树的最近公共祖先
剑指 Offer 34. 二叉树中和为某一值的路径
124. 二叉树中的最大路径和
难度中等101收藏分享切换为英文接收动态反馈
层序遍历
简单来说就是一行一行地遍历,基于队列来做,先把根节点入队列,只要队列非空,每次把队头结点弹出,然后把堆头的左右节点压入队列中,这样最终遍历出来的就是层序遍历的顺序。
function levelOrder(root) {
if (root == null)
return null;
let res = new Array();
let queue = new Array();
queue.unshift(root); // 先把根节点入队列
while (queue.length > 0) { // 队列非空
root = queue.pop();
res.push(root.val); // 弹出队头节点
if (root.left != null) queue.unshift(root.left);
if (root.right != null) queue.unshift(root.right);
}
return res;
}
上面是一种比较简单的实现,只能按顺序进行遍历,例如[1, 2, 3, 4, 5, 6, 7]
,无法获取每一层的节点。
如果需要单独打印出每一层的节点,可以使用下面的写法:
function levelOrder(root) {
if (root == null)
return null;
let res = new Array();
let queue = new Array();
queue.unshift(root);
while (queue.length > 0) {
let size = queue.length, // 队列保存了当前层的节点,获取节点个数
temp = []; // 临时保存当前层节点的值
for(let i=0; i<size; i++) { // for循环将当前层节点全部出队,并将下一层的节点加入队列
root = queue.pop();
temp.push(root.val);
if (root.left != null) queue.unshift(root.left);
if (root.right != null) queue.unshift(root.right);
}
res.push(temp);
}
return res
}
在上面的代码中,while
每循环一次,就会遍历二叉树的一层,输出的结果为[ [ 1 ], [ 2, 3 ], [ 4, 5, 6, 7 ] ]
,掌握上面这个代码,下面的问题就不在话下了:
102. 二叉树的层序遍历
104. 二叉树的最大深度
111. 二叉树的最小深度
199. 二叉树的右视图
637. 二叉树的层平均值
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。