【剑指 の 精选】从宏观角度看「对称二叉树」问题 |Python 主题月

简介: 【剑指 の 精选】从宏观角度看「对称二叉树」问题 |Python 主题月

网络异常,图片无法展示
|


题目描述



这是「牛客网」上的「JZ 58 对称的二叉树」,难度为「困难」。


Tag : 「剑指 Offer」、「二叉树」、「层序遍历」、「迭代」、「递归」


描述:


请实现一个函数,用来判断一棵二叉树是不是对称的。


注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。


示例1


输入:{8,6,6,5,7,7,5}
返回值:true
复制代码


示例2


输入:{8,6,9,5,7,7,5}
返回值:false
复制代码


要求:


  • 时间:1 s
  • 空间:64 M


基本思想



首先要明确,题目所定义的 “对称” 是对每层而言,同时考虑空节点。


因此,如果我们使用常规的遍历方式进行检查的话,需要对空节点有所表示。


局部检查(层序遍历)



我们使用 0x3f3f3f3f 作为无效值,并建立占位节点 emptyNode 用来代指空节点(emptyNode.val = 0x3f3f3f3f)。


一个朴素的做法是:使用「层序遍历」的方式进行「逐层检查」,对于空节点使用 emptyNode 进行代指,同时确保不递归 emptyNode 对应的子节点。


具体做法如下:


  1. 起始时,将 root 节点入队;
  2. 从队列中取出节点,检查节点是否为 emptyNode 节点来决定是否继续入队:
  • 当不是 emptyNode 节点时,将其左/右儿子进行入队,如果没有左/右儿子,则用 emptyNode 代替入队;
  • 当是 emptyNode 节点时,则忽略;
  1. 在进行流程 的同时使用「临时列表」记录当前层的信息,并检查当前层是否符合 “对称” 要求;
  2. 循环流程 和 ,直到整个队列为空。


Java 代码:


import java.util.*;
class Solution {
    int INF = 0x3f3f3f3f;
    TreeNode emptyNode = new TreeNode(INF);
    boolean isSymmetrical(TreeNode root) {
        if (root == null) return true;
        Deque<TreeNode> d = new ArrayDeque<>();
        d.add(root);
        while (!d.isEmpty()) {
            // 每次循环都将下一层拓展完并存到「队列」中
            // 同时将该层节点值依次存入到「临时列表」中
            int size  = d.size();
            List<Integer> list = new ArrayList<>();
            while (size-- > 0) {
                TreeNode poll = d.pollFirst();
                if (!poll.equals(emptyNode)) {
                    d.addLast(poll.left != null ? poll.left : emptyNode);
                    d.addLast(poll.right != null ? poll.right : emptyNode);
                }
                list.add(poll.val);
            }
            // 每一层拓展完后,检查一下存放当前层的该层是否符合「对称」要求
            if (!check(list)) return false;
        }
        return true;
    }
    // 使用「双指针」检查某层是否符合「对称」要求
    boolean check(List<Integer> list) {
        int l = 0, r = list.size() - 1;
        while (l < r) {
            if (!list.get(l).equals(list.get(r))) return false;
            l++;
            r--;
        }
        return true;
    }
}
复制代码


Python 3 代码:


from collections import deque
from math import inf
class Solution:
    emptyNode = TreeNode(inf)
    def isSymmetrical(self, root):
        if root is None:
            return True
        d = deque([])
        d.append(root)
        while d:
            # 每次循环都将下一层拓展完并存到「队列」中
            # 同时将该层节点值依次存入到「临时列表」中
            size = len(d)
            lt = []
            while size > 0:
                poll = d.popleft()
                if poll != self.emptyNode:
                    d.append(poll.left if poll.left is not None else self.emptyNode)
                    d.append(poll.right if poll.right is not None else self.emptyNode)
                size -= 1
                lt.append(poll.val)
            # 每一层拓展完后,检查一下存放当前层的该层是否符合「对称」要求
            if not self.check(lt):
                return False
        return True
    def check(self, lt):
        # 使用「双指针」检查某层是否符合「对称」要求
        l, r = 0, len(lt) - 1
        while l < r:
            if lt[l] != lt[r]:
                return False
            l += 1
            r -= 1
        return True
复制代码


  • 时间复杂度:在层序遍历过程中,每个节点最多入队一次,同时在 check 检查对称性过程中,每层只会被检查一次。复杂度为 O(n)
  • 空间复杂度:O(n)


整体检查(递归)



在「层序遍历」解法中,我们利用了 “对称” 定义对每层进行检查。


本质上这是利用 “对称” 定义进行多次「局部」检查。


事实上,我们还可以利用 “对称” 定义在「整体」层面上进行检查。


我们如何定义两棵子树 ab 是否 “对称” ?


当且仅当两棵子树符合如下要求时,满足 “对称” 要求:


  1. 两棵子树根节点值相同;
  2. 两颗子树的左右子树分别对称,包括:
  • a 树的左子树与 b 树的右子树相应位置的值相等
  • a 树的右子树与 b 树的左子树相应位置的值相等

网络异常,图片无法展示
|


具体的,我们可以设计一个递归函数 check ,传入待检测的两颗子树的头结点 ab(对于本题都传入 root 即可),在单次查询中有如下显而易见的判断子树是否 “对称” 的 Base Case:


  • ab 均为空节点:符合 “对称” 要求;
  • ab 其中一个节点为空,不符合  “对称” 要求;
  • ab 值不相等,不符合  “对称” 要求;


其他情况,我们则要分别检查 ab 的左右节点是否 “对称” ,即递归调用 check(a.left, b.right)check(a.right, b.left)


Java 代码:


class Solution {
    public boolean isSymmetrical(TreeNode root) {
        return check(root, root);
    }
    boolean check(TreeNode a, TreeNode b) {
        if (a == null && b == null) return true;
        if (a == null || b == null) return false;
        if (a.val != b.val) return false;
        return check(a.left, b.right) && check(a.right, b.left);
    }
}
复制代码


Python 3 代码:


class Solution:
    def isSymmetrical(self, root):
        return self.check(root, root)
    def check(self, a, b):
        if a is None and b is None:
            return True
        elif a is None or b is None:
            return False
        if a.val != b.val:
            return False
        return self.check(a.left, b.right) and self.check(a.right, b.left)
复制代码


  • 时间复杂度:每个节点只会被访问一次。复杂度为 O(n)
  • 空间复杂度:忽略递归带来的额外空间开销。复杂度为 O(1)


总结



上述两种解法不仅仅是实现上的不同,更多的是检查 “出发点” 的不同:


  • 解法一:利用「层序遍历」的方式,以 “层” 为单位进行 “对称” 检查;
  • 解法二:利用「递归树展开」的方式,以 “子树” 为单位进行 “对称” 检查。


当我们从整体层面出发考虑时,配合递归,往往能写出比常规做法要简洁得多的代码。


建议大家加深对「局部」和「整体」两种不同出发点的理解。


最后



这是我们「剑指 の 精选」系列文章的第 58 篇,系列开始于 2021/07/01。


该系列会将「剑指 Offer」中比较经典而又不过时的题目都讲一遍。


在提供追求「证明」&「思路」的同时,提供最为简洁的代码。


欢迎关注,交个朋友 (`・ω・´)

相关文章
|
20天前
|
数据采集 自然语言处理 算法
如何使用Python的Gensim库进行自然语言处理和主题建模?
使用Gensim库进行Python自然语言处理和主题建模,包括:1) 安装Gensim;2) 导入`corpora`, `models`, `nltk`等相关模块;3) 对文本数据进行预处理,如分词和去除停用词;4) 创建字典和语料库;5) 使用LDA算法训练模型;6) 查看每个主题的主要关键词。代码示例展示了从数据预处理到主题提取的完整流程。
37 3
|
19天前
|
自然语言处理 数据可视化 算法
Python主题建模LDA模型、t-SNE 降维聚类、词云可视化文本挖掘新闻组数据集
Python主题建模LDA模型、t-SNE 降维聚类、词云可视化文本挖掘新闻组数据集
|
24天前
|
数据可视化 算法 数据挖掘
Python主题建模LDA模型、t-SNE 降维聚类、词云可视化文本挖掘新闻组数据集2
Python主题建模LDA模型、t-SNE 降维聚类、词云可视化文本挖掘新闻组数据集
|
24天前
|
自然语言处理 数据可视化 算法
Python主题建模LDA模型、t-SNE 降维聚类、词云可视化文本挖掘新闻组数据集1
Python主题建模LDA模型、t-SNE 降维聚类、词云可视化文本挖掘新闻组数据集
|
25天前
|
数据可视化 算法 编译器
python主题LDA建模和t-SNE可视化
python主题LDA建模和t-SNE可视化
|
25天前
|
自然语言处理 数据可视化 Python
python主题建模可视化LDA和T-SNE交互式可视化
python主题建模可视化LDA和T-SNE交互式可视化
|
27天前
|
测试技术 Python
Python 高级主题:如何实现一个简单的 Python 单元测试?
Python单元测试示例:使用`unittest`模块测试`my_function`函数。定义函数`my_function(x)`返回`x*2`,然后创建`TestMyFunction`类继承`unittest.TestCase`,包含两个测试方法检验不同输入。通过`unittest.main()`运行测试。遵循小写字母命名测试方法和使用断言检查结果的最佳实践。可选`pytest`等第三方库进行复杂测试。
13 1
|
28天前
|
Python
Python 高级主题:什么是 Python 中的装饰器函数?
Python装饰器是一种特殊函数,用于在不修改原代码的情况下为函数增添功能。它们接收一个函数作为参数并返回一个新的函数,常在原函数前后添加额外操作。例如,`outer`装饰器会在`foo`函数执行前后打印信息并修改其返回值。调用`foo()`实则执行了装饰后的`inner`函数。
14 5
|
28天前
|
JavaScript 前端开发 Python
Python 高级主题: 解释 Python 中的闭包是什么?
【4月更文挑战第13天】闭包是内部函数引用外部变量的函数对象,作为外部函数的返回值。当外部函数执行完毕,其变量本应消失,但由于内部函数的引用,这些变量在内存中保持存活,形成闭包。例如,在外函数中定义内函数并返回内函数引用,实现对外部局部变量的持久访问。闭包在Python和JavaScript等语言中常见,是强大的编程工具,连接不同作用域并允许局部变量持久化,用于复杂程序设计。**
16 4
|
3月前
|
Python
使用python写一个二叉树
使用python写一个二叉树
25 1