二叉树遍历,你真的学会了吗 | 附微软面经
厚积薄发,向阳生长。
--题记
2020年5月20号我参加了微软的SWE暑期实习生面试,3月中旬投了简历,微软的笔试安排在3月25号,但我当时并没有收到笔试通知,以为暑期实习无缘微软了,但又收到了微软的面试通知,HR告知可以直接参加面试,当时有收到微软面试通知的信息,也有对自己算法能力的担忧。
前端小白从3月份开始准备面试,经历了煎熬的4月,期间不断地投递简历,参加笔试、面试,功夫不负有心人,抓住了春招的尾巴,四月底开始陆陆续续收到快手、字节、作业帮、京东、网易雷火的offer,之后对复习就没有那么上心了,所以面对微软的面试很担心算法这方面。
既然已经收到了offer,那为什么还要参加微软的面试呢?一来是既然投递了简历,能有一次面试机会,那就不要错过,长长见识也好;二来,也是很重要的,找工作是面向秋招的,互联网的压力程序员都会懂,相较之下外企就能够很好地保证work&life balance,所以也要积累经验,为秋招做准备。
微软的技术岗面试一共有3轮,前两轮只要拿到一个hire,就可以进到第三轮,所以不存在第一轮面挂的现象,都会进入到第二轮。
微软的面试经历给我的感觉就是,二叉树遍历,只会递归是不够的,掌握非递归遍历方式才是王道。所以这篇文章,我主要是想讲一下二叉树3种遍历方式的非递归实现。微软两轮面试中都考察了二叉树的非递归遍历,而且面试官深究细节,所以在准备面试微软的同学,一定不要错过这篇文章哦,也许下一次面试你就遇到了相同的算法题目。
二叉树结构
下图所示为一个二叉树:
二叉树节点
function Node(val) {
this.val = val;
this.left = null;
this.right = null;
}
二叉树遍历
按照根节点的访问顺序的不同,二叉树常见的遍历方式分为三种:前序、中序和后序。对于上图的二叉树,三种遍历方式的访问顺序分别是:
前序:ABDCEF
中序:DBAECF
后序:DBEFCA
接下来分别说明二叉树的递归遍历和非递归遍历实现。
01
对于三种遍历的递归方式,相信大家都能很快写出来:
前序
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);
}
那么非递归遍历呢,能不能很快写出来?
不要急,下面我会一步步带你把三种方式的非递归遍历整明白
02
前序
前序遍历的实现:
1) 判断根节点是否为空,如果为空,则返回;不为空,则入栈;
2) 对栈进行判断,如果栈不为空,则栈顶元素出栈,访问栈顶元素;
3) 对子节点进行判断,如果子节点不为空,则入栈,需要注意的是,右节点先入栈,左节点后入栈,这样能够保证节点的访问顺序为左节点先于右节点
代码实现:
var preorder_non_recursive = function (node) {
if (node === null) return;
let stack = [];
let p = node;
if (p !== null) {
stack.push(p);
while (stack.length > 0) {
p = stack.pop();
console.log(p.val);
if (p.right !== null) {
stack.push(p.right);
}
if (p.left != null) {
stack.push(p.left);
}
}
}
}
关于前序遍历的非递归还有另一种写法,循环条件为栈不为空或节点不为空,具体过程为:
1) 如果栈不为空或节点不为空,首先对节点进行判断,如果节点不为空,则访问节点,同时向左遍历到该节点的最左子树,遍历的同时将节点入栈;
2) 遍历到最左侧,如果栈不为空,则栈顶元素出栈,对栈顶元素的右节点进行上述判断;
说明:栈用于记录访问顺序,每次将栈顶元素弹出之后访问其右子树
var preorder_non_recursive = function(node){
if (node === null) return;
let stack = [];
let p = node;
while (stack.length > 0 || p !== null) {
while (p !== null) {
console.log(p.val);
stack.push(p);
p = p.left;
}
if (stack.length > 0) {
p = stack.pop();
p = p.right;
}
}
}
中序
中序遍历的思想是先遍历到根节点的最左侧,之后栈顶元素弹出,访问,再对栈顶元素的右子树进行判断。
算法过程:
1) 如果栈不为空或节点不为空,首先对节点进行判断,如果节点不为空,则向左遍历到该节点的最左子树,遍历的同时将节点入栈;
2) 遍历到最左侧,如果栈不为空,则栈顶元素出栈,访问栈顶元素,对栈顶元素的右节点进行上述判断;
注意:遍历过程跟先序遍历的第二种实现方式类似,不同的是访问元素的位置,先序遍历是先访问元素,再入栈,中序遍历是直接入栈,指针走到最左侧再弹出栈顶元素进行访问
代码实现:
var inorder_non_recursive = function(node){
if (node === null) return;
let stack = [];
let p = node;
while (stack.length > 0 || p !== null) {
while (p !== null) {
stack.push(p);
p = p.left;
}
if (stack.length > 0) {
p = stack.pop();
console.log(p.val);
p = p.right;
}
}
}
后序
后序遍历的实现相对先序遍历和中序遍历相对复杂,不过可以借助先序遍历的思维去实现,先序遍历是根左右,我们可以先实现根右左的遍历,再借助另一个栈逆序输出,得到左右根的顺序。
代码实现:
// 两个栈实现非递归
var postorder_non_recursive = function(node) {
if (node === null) {
return;
}
let p = node;
let stack = [];
let output = [];
while (p !== null || stack.length > 0) {
if (p !== null) {
stack.push(p);
// 另一个栈存放元素:中右左
output.push(p);
p = p.right; // 遍历到最右
} else {
p = stack.pop();
p = p.left;
}
}
while (output.length > 0) {
console.log(output.pop().val);
}
}
关于二叉树的部分就这些了,那关于二叉树的非递归遍历实现,你学会了吗?
除了以上提到的三种遍历方式,二叉树的遍历还有层次遍历,从根节点开始,从上到下,从左向右依次访问节点,也是很经典的算法,关于层次遍历我们会在后续的算法系列中进行讲解。
最后附上微软的算法题:
一面:
最长回文子串
中序遍历递归和非递归
中序遍历的下一个子节点
二面:
单链表逆序
二进制转十进制,考虑溢出
将二叉树当前节点的值更新为当前节点所有子节点的值之和(递归和非递归)
最后我想说,如果这篇文章对你有帮助,那就请你帮我三个小忙:
1.点击「在看」,表示你对这篇文章的认可❤️❤️❤️
3.向你周围的朋友推荐「我是前端喵」,一起进步吧🌞🌞🌞
我知道你在看哟