算法题解---翻转二叉树

89 阅读6分钟

前言

之前我更新了一篇文章,其中详细介绍了二叉树的理论基础,包含二叉树的定义,类型,以及二叉树的几种遍历方式,这篇文章,通过一道应用题来,使用三种不同的方式来进行处理,让大家对二叉树的不同遍历方式的特点有更详细的了解

本文的算法解答均为JS代码,在解答的同时会结合JS的一些特性,适合JS的学者进行参考!


请听题

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

image.png


分析题目

我们从上面的图片可以看到,本题可以通过将每个节点的左右孩子节点进行交换,可以达到题目的翻转效果,所以我们需要想办法遍历到每个节点,然后对每个节点进行交换的操作。所以我们的思路变成了

1.如何遍历每个节点,2.在遍历过程中如何对每个节点进行操作。


做法一:递归

遍历的话咱们首先使用递归遍历,那么到底是使用前、中、后序的哪一种呢,我们分析之后可以得到, 可以选择前序遍历和后序遍历的任意一种,但是不能使用中序遍历!

前序遍历

前序遍历,使用“根,左,右”的顺序,整个翻转的过程就是从上而下,先对根节点进行翻转处理,接着对左右节点进行翻转处理。

image.png

咱们按照递归三部曲来

  1. 确定递归函数的参数和返回值:由于是遍历,参数就是节点,实际上我们是完成翻转操作,意味着只涉及到内部指针的改变,所以不需要返回值
  2. 确定终止条件:当节点为null时,不需要再翻转了,此时进行return
  3. 确定单层递归的逻辑:前序遍历,先是根节点,后左右节点。

给出代码:

/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function(root) {
    
    const dfs = function(node){
        if(node === null) return;
        [node.left,node.right] = [node.right,node.left];  //根节点处理
        dfs(node.left);  //递归左节点处理
        dfs(node.right);  //递归右节点处理      
    }
    dfs(root);
    return root;
}

后序遍历

后序遍历使用左、右、根的顺序,分析方法同上,后序遍历的代码可以由前序遍历的代码更改而来

给出代码:

/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function(root) {
    
    const dfs = function(node){
        if(node === null) return;
        dfs(node.left);  //递归左节点处理
        dfs(node.right);  //递归右节点处理      
        [node.left,node.right] = [node.right,node.left];  //根节点处理
    }
    dfs(root);
    return root;
}

为什么递归不能使用中序遍历?

因为递归中,使用中序遍历的话,某些节点的左右节点会翻转两次

这里举一个例子(画图丑,大家凑合看)

930a09aad36a87b66b67fc4d95f12ac.jpg

所以我的建议是用栈替代递归,此时可以用栈的解构来实现中序遍历,详见下面。


做法二:用栈替代递归

有了做法一的递归,用栈替代递归是我们面试大厂的核心技能,往往面试官看你用栈很快地解出了答案,则会问你栈的做法。

这里可以参考我上一篇文章,里面提到了对二叉树的递归,给出了一个很好用的栈替代写法,并且还能通过简单更改代码来进行更改遍历顺序的效果。

实际上用栈的话,前、中、后三种方式都能进行遍历并且翻转,这里给出前序遍历,注意中序遍历和后序遍历只需要简单更改根节点翻转的位置即可!

    var invertTree = function(root) {
    const stack = [];
    if(root)
    {
        stack.push({node:root,visited:false});
    }
    while(stack.length)
    {
        let {node,visited} = stack.pop(); //解构表达式
        if(visited)
        {
            [node.left, node.right] = [node.right, node.left]; // ES6 解构赋值
        }
        else
        {   
        //这里是 前序遍历 , 对于中序遍历和后序遍历,只需要更改stack.push操作的位置即可
            stack.push({node:node,visited:true})
            if(node.right) stack.push({node:node.right,visited:false});
            if(node.left) stack.push({node:node.left,visited:false});        
        }
    }
    return root;
}

栈可以用中序遍历

注意,用栈是可以用中序遍历的,因为在栈中,按照我们的算法,始终能够按照正确的节点遍历顺序进行!

0f8811a82c7284f5cb0113f73df292e.jpg

20f7351903df82fee39beda9b8d0b50.jpg


做法三:层序遍历

因为我们了解到题目要求对每个结点进行翻转操作即可,所以我们也可以用层序遍历从上到下进行遍历每个结点并且进行翻转,是没问题的

给出代码:

/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function(root) {

    const queue = [];
    if(root) {queue.push(root)}
    while(queue.length)
    {
        let size = queue.length;
        while(size--)
        {
            let node = queue.shift();
            [node.left,node.right] = [node.right,node.left]
            node.left && queue.push(node.left);
            node.right && queue.push(node.right); 
        }
    }
    return root;

};


JS启示

swap的实现

在做这道题时,有些人可能会想着将左右孩子结点交换的操作封装成一个函数,于是就有了

const swap = function(a,b){
    let t;
    t = b;
    b = a;
    a = t;
}

swap(node.left,node.right)

千万要注意,在JS中,这样子不行的。

首先, node.left 和 node.right 是对象的引用(指针),例如:

  • node.left 存储的是 TreeNode A 的内存地址(比如 0x123)。
  • node.right 存储的是 TreeNode B 的内存地址(比如 0x456)。

调用 swap(node.left, node.right) 时

  • a 和 b 是函数局部变量,接收的是 node.left 和 node.right 的 值的副本(即 0x123 和 0x456 的副本)。
  • 在 swap 内部修改 a 和 b 只会影响局部变量,不会影响外部的 node.left 和 node.right

我们想要正确交换的话,就不要操作局部变量,而是直接修改node的属性

const temp = node.left;
node.left = node.right;
node.right = temp;

解构赋值

咱们也可以使用ES6的新特性来实现。

[node.left,node.right] = [node.right,node.left]; //直接进行交换

ES6 的解构赋值提供了一种 简洁、高效的方式交换变量,无需临时变量,适用于 基本类型 和 对象/数组属性。它的核心原理是 先构造一个临时数组/对象,再按模式解构赋值


使用ES6交换基础值

const obj = { left: "A", right: "B" };

[obj.left, obj.right] = [obj.right, obj.left];

console.log(obj.left);  // "B"
console.log(obj.right); // "A"

使用ES6交换对象

let a = 1;
let b = 2;

[a, b] = [b, a]; // 解构赋值交换

console.log(a); // 2
console.log(b); // 1

咱们的代码中还有一处也用到了这种解构赋值,与上面不同的是,使用的是对象的解构, 注意左边的变量名需要使用对象的属性名,并且用大括号{}而非中括号[],可以很方便地将stack的栈顶的对象的值直接传给我们的变量,这个用法在我用JS中非常常用!

let {node,visited} = stack.pop(); //直接将栈顶的对象的值传递给node、visited变量

总结

本文通过一道题,二叉树的应用中,三种遍历模式的实现方式进行了对比和分析,分析了不同的方式的特点。并且还对JS中ES6引入的解构赋值进行了详细讲解,希望对你有帮助。


🌇结尾

本文部分内容参考卡尔的:代码随想录

感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)

作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。