vlambda博客
学习文章列表

二叉树遍历,你真的学会了吗 | 附微软面经





厚积薄发,向阳生长。

--题记


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.向你周围的朋友推荐「我是前端喵」,一起进步吧🌞🌞🌞


我知道你在看