【算法-LeetCode】101. 对称二叉树(二叉树;递归;层序遍历;回文)

101. 对称二叉树 - 力扣(LeetCode)

文章更新:2021年10月25日11:04:59

问题描述及示例

给定一个二叉树,检查它是否是镜像对称的。

示例 1:
输入:root = [1,2,2,3,4,4,3]

    1
   / \
  2   2
 / \ / \
3  4 4  3

输出:true

示例 2:
输入:root = [1,2,2,null,3,null,3]

    1
   / \
  2   2
   \   \
   3    3

输出:false

我的题解

我的题解1(暴力解法;翻转与遍历)

思路非常简单,就是先遍历一遍原二叉树并存储遍历结果在 origin 中,再将二叉树对称翻转,再次进行遍历并将遍历结果存储在 inverted 中,然后再逐个比较 origininverted 中的元素,如果发现翻转之后的遍历结果和原遍历结果一样,就说明当前二叉树是对称的。其中翻转二叉树和遍历二叉树的思路可以参看我此前的博客:

参考:【算法-LeetCode】226. 翻转二叉树(二叉树;递归)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】144. 二叉树的前序遍历(二叉树;递归;LeetCode的运行机制)_赖念安的博客-CSDN博客

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isSymmetric = function(root) {
  // traverseResult用于存储遍历的结果
  let traverseResult = [];
  // 先遍历一次原二叉树
  traverseTree(root);
  // 将遍历结果存在orgin中,注意这里是利用展开运算符实现的深拷贝
  let origin = [...traverseResult];
  // 再翻转二叉树
  invertTree(root);
  // 在使用traverseResult前先对其清空重置
  traverseResult.length = 0;
  // 再次遍历翻转后的二叉树
  traverseTree(root);
  // 遍历翻转后二叉树并将结果存在inverted中
  let inverted = [...traverseResult];

  // 对比原二叉树和翻转后的二叉树的遍历结果
  return origin.every((cur,idx) => cur === inverted[idx]);
  
  // traverseTree用于遍历一棵二叉树,并将遍历结果存在函数外部的traverseResult中
  // 详解可参看上方相关博客
  function traverseTree(root) {
    if(!root) {
      // 注意,为了保证判断的正确性,应该将空节点也计入遍历结果中
      traverseResult.push(null);
      return;
    }
    traverseResult.push(root.val);
    traverseTree(root.left);
    traverseTree(root.right);
  }

  // invertTree用于对称翻转一棵二叉树,详解可看上方相关博客
  function invertTree(root) {
    if(!root) {
      return null;
    }
    [root.left, root.right] = [root.right, root.left];
    invertTree(root.left);
    invertTree(root.right);
  }
};


提交记录
197 / 197 个通过测试用例
状态:通过
执行用时:96 ms, 在所有 JavaScript 提交中击败了9.73%的用户
内存消耗:40.4 MB, 在所有 JavaScript 提交中击败了5.15%的用户
时间:2021/10/25 11:17

当然,在意料之中,这种解法的时间表现和空间表现都不怎么样。

我的题解2(层序遍历与回文判断)

主要思路就是在层序遍历的基础上对每一层的遍历结果做回文判断,因为如果一棵二叉树是对称的,那么其每一层的遍历结果一定是回文序列。有关层序遍历和回文判断的思路可以参看下方博客:

参考:【算法-LeetCode】102. 二叉树的层序遍历(二叉树;层序遍历;BFS;生成二叉树)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】9. 回文数(数组方法)_赖念安的博客-CSDN博客_回文数数组

注意,因为本题需要判断二叉树对称,所以需要在原来层序遍历的基础上做一点修改:把空节点 null 也算入层序遍历的结果中。也就是把整棵二叉树中没画出来的空节点也当成是一个有效节点补全二叉树。这点比较关键,如果不算空节点的话,会对后续的回文判断产生影响。

在尝试之前,因为题目没说 root 可否为空,所有我还特意测试了一下,发现 root 不为能空:
在这里插入图片描述
在这里插入图片描述

成功前的尝试

基础框架还是层序遍历的逻辑,这里不再多解释了,上面的博客中有详细的描述。

var isSymmetric = function(root) {
  let queue = [root.left, root.right];
  while(queue.length) {
    // level存储当前层的所有节点,注意是节点对象,而非节点值
    let level = [];
    for(let i = 0, len = queue.length; i < len; i++) {
      let node = queue.shift();
      // nodeVal是当前遍历的节点的节点值,如果不为空则取node.val,若为空则取null
      let nodeVal = node ? node.val : null;
      // topVal是level栈顶元素的节点值
      let topVal = level[level.length - 1] ? level[level.length - 1].val : null;
      // 如果level为空,或者当前遍历的节点值不等于level栈顶元素的值则将当前节点压入level
      if(!level.length || nodeVal !== topVal) {
        level.push(node);
      } else {
        // 否则的话,将level栈顶元素弹出
        level.pop();
      }
      // 如果当前节点不为空,则一定有左右子节点(虽然这些子节点可能为null)
      if(node) {
        // 这当前节点的额左右子节点入列
        queue.push(node.left, node.right);
      }
    }
    // 如果遍历完当前层的节点,发现level中还剩余元素,则说明当前层不满足对称二叉树的特点
    // 此时可以直接返回false
    if(level.length) {
      return false;
    }
  }
  // 如果每一层都满足回文特点,那么返回true
  return true;
};


提交记录
通过测试用例:196 / 197
输入:[2,3,3,4,5,5,4,null,null,8,9,null,null,9,8]
输出:true
预期结果:false
时间:2021/10/25 14:27	

在这里插入图片描述
只剩上面这个用例没有通过,看来判断逻辑还是有待改进。可以发现,如果当前层中出现了有两个相邻节点的节点值是一样的(或者同为 null,即上面这个用例的情况),那么程序就不能有效判断对称情况。

层序遍历与回文判断

经过一番调试,我发现我上面的那种形式其实是做括号匹配的时的思想,但是其实本题需要用到的是回文判断的思想。所以我将判断逻辑修改了一下。括号匹配的思路可以参看下面的博客:

参考:【算法-LeetCode】20. 有效的括号(栈)_赖念安的博客-CSDN博客

大部分的逻辑还是和上面一样的,只不过对某一层的回文判断做了修改。原先是每遍历一个节点就判断,现在是等 level 中存储了当前层一半的节点(值)时再开始回文判断。

var isSymmetric = function(root) {
  let queue = [root.left, root.right];
  while(queue.length) {
    let level = [];
    for(let i = 0, len = queue.length; i < len; i++) {
      // node是当前遍历节点
      let node = queue.shift();
      // 因为后面有continue语句,所以把入队逻辑提到前面来
      if(node) {
        queue.push(node.left, node.right);
      }
      let nodeVal = node ? node.val : null;
      // 如果当前还没遍历到当前层的一半,则直接将当前节点的节点值压入level即可
      if(i < len / 2) {
        level.push(nodeVal);
        // continue用于忽略后面的判断逻辑,此时还不到判断的时候
        continue;
      } 
      // 当开始遍历当前层的后面一半节点时,开始判断当前层是否有回文特征
      if(nodeVal !== level[level.length - 1]) {
        // 一旦发现当前层不满足回文特征了,可以立马返回false,
        // 不必等到遍历完所有的当前层节点再返回,这也算是一种优化
        return false;
      } else {
        // 如果当前遍历节点值与level栈顶元素相等的话,将原先level栈顶的元素弹出
        level.pop();
      }
    }
  }
  return true;
};


提交记录
197 / 197 个通过测试用例
状态:通过
执行用时:92 ms, 在所有 JavaScript 提交中击败了14.74%的用户
内存消耗:39.9 MB, 在所有 JavaScript 提交中击败了59.32%的用户
时间:2021/10/25 15:13

官方题解

更新:2021年7月29日18:43:21

因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。

更新:2021年10月25日10:01:03

参考:跳跃游戏 - 跳跃游戏 - 力扣(LeetCode)

【更新结束】

有关参考

更新:2021年10月25日11:22:49
参考:【算法-LeetCode】226. 翻转二叉树(二叉树;递归)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】144. 二叉树的前序遍历(二叉树;递归;LeetCode的运行机制)_赖念安的博客-CSDN博客
更新:2021年10月25日15:19:58
参考:【算法-LeetCode】102. 二叉树的层序遍历(二叉树;层序遍历;BFS;生成二叉树)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】20. 有效的括号(栈)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】9. 回文数(数组方法)_赖念安的博客-CSDN博客_回文数数组

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值