DAY20:二叉树(十)最大二叉树+合并二叉树

news2024/11/21 2:37:17

文章目录

    • 654.最大二叉树
      • 思路
        • 遍历顺序
      • 完整版
        • 变量作用域的问题
      • 修改后的完整版
        • 递归进一步理解
        • 关于终止条件
      • 优化
        • 时间复杂度和空间复杂度的优化
        • 补充:二叉树的高度logn
    • 617.合并二叉树
      • 思路
      • 完整版
      • 定义新二叉树的写法

654.最大二叉树

  • 本题做的时候也卡了一些问题,详见上一篇博客。同时还要注意变量的块作用域规则,很基础但是易出错
  • 代码优化方面,优化方法要掌握
  • 注意本题终止条件写法不同,递归数组不一定要判空

给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:

创建一个根节点,其值为 nums 中的最大值。
递归地在最大值 左边 的 子数组前缀上 构建左子树。
递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums 构建的 最大二叉树 。
在这里插入图片描述
输入:nums = [3,2,1,6,0,5]
输出:[6,3,5,null,2,0,null,null,1]
解释:递归调用如下所示:

  • [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5] 。
    • [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。
      • 空数组,无子节点。
      • [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1] 。
        • 空数组,无子节点。
        • 只有一个元素,所以子节点是一个值为 1 的节点。
    • [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 [] 。
      • 只有一个元素,所以子节点是一个值为 0 的节点。
      • 空数组,无子节点。
        在这里插入图片描述
        输入:nums = [3,2,1]
        输出:[3,null,2,null,1]

思路

这道题和上一道 106.中序和后序遍历构造二叉树 类似。

本题是找数组的最大值作为根节点,然后分割最大值左右两侧,将左侧分为左子树区间,右侧分为右子树区间,再在左右两侧区间内找到最大值作为左孩子和右孩子。对于左右孩子也递归地进行找最大值的操作。

遍历顺序

凡是构造二叉树类的题目,都要用前序遍历。106.中序和后序遍历构造二叉树 实际上也是前序遍历,先进行逻辑处理,再处理左右节点。构造的时候要先把根节点构造出来,再递归构造左右子树

完整版

  • 递归终止条件的考虑一般都是直接考虑递归结束逻辑+极端情况,比如本题就是nums数组是空的,106就是直接考虑数组大小是0
  • root本身就是指针类型,因此nums是空的情况直接返回空指针nullptr即可
  • 不能在if里面重新声明vectorleft(nums.begin(),nums.begin()+maxIndex),这样的话重新声明的left数组在if语句块之外就会被销毁。
TreeNode* contruct(vector<int>& nums){
    //终止条件:数组内没有没遍历到的元素了,也就是切出来的左区间和右区间都是NULL
    //也可以写成,nums数组已经空了,因为每次递归都会减去最大元素
    
    //终止条件的考虑一般都是考虑代码运行逻辑+极端情况
    if(nums.empty()){
        return nullptr; //如果nums是空的,直接返回空指针
    }
    
    //找最大值及其下标
    int maxValue=INT_MIN;
    int index;
    int maxIndex;
    for(index=0;index<nums.size();index++){
        if(nums[index]>maxValue){
            maxValue = nums[index];//最大值
            maxIndex = index;//最大值的下标
        }
    }
    //找到最大值之后,根节点数值确定
    TreeNode* root = new TreeNode(maxValue);
    
    //分割数组
    //注意:1.最大值本身要在新的数组里去掉,所以right左闭的部分应该+1
    //2.不能操作空数组,maxIndex这里可能是0,是0的话就会发生操作空数组报错。但是我们的终止条件是if(nums.empty()),所以并不需要考虑操作空数组的问题
    
    //左数组,左闭右开
    vector<int>left;
    //这里的写法是有问题的,在if里面重新声明了left数组,这个重新声明的数组在if语句块之外就会被销毁!
    if(nums.begin()!=nums.begin()+maxIndex){
        vector<int>left(nums.begin(),nums.begin()+maxIndex);
    }
    //右数组,左闭右开
    vector<int>right;
    if(nums.maxIndex+1!=nums.end()){
        vector<int>right(nums.begin()+maxIndex+1,nums.end());
    }
    
    //分割数组后进行左右子树的递归,注意这里要判断left是不是空的!
    root->left = contruct(left);
    root->right = contruct(right);
    return root;
}

变量作用域的问题

(实际上因为终止条件写的是判定空数组,因此这里根本不需要加if条件,但是变量作用域的问题也是一个bug点)

最开始的写法是:

    //左数组,左闭右开
	vector<int>left;
    if(nums.begin()!=nums.begin()+maxIndex){
        //问题就在于此处重新声明了,重新声明的left在if之外就销毁了,if之外的left并没有改变
        vector<int>left(nums.begin(),nums.begin()+maxIndex);
    }
    //右数组,左闭右开
	vector<int>right;
    if(nums.maxIndex+1!=nums.end()){
        vector<int>right(nums.begin()+maxIndex+1,nums.end());
    }
    //分割数组后进行左右子树的递归
    if(!left.empty()){
        root->left = contruct(left);
    }
    if(!right.empty()){
        root->right = contruct(right);
    }

但是,这样的写法存在left和right变量作用域的问题。

在C++中,变量的生命周期由其作用域决定在if语句块(以及所有其他类型的块,例如while、for循环和自定义块)中声明的变量只在该块内部有效。一旦代码执行离开了该块,块内声明的所有变量都将被销毁

因此,if 语句内部创建 leftright 向量,然而这样创建的向量只在 if 语句块内有效,出了 if 语句块,这些向量就会被销毁,就会导致:

    //分割数组后进行左右子树的递归
    root->left = contruct(left);
    root->right = contruct(right);

这一部分的递归,找不到 leftright 数组,因为这两个数组已经在if语句块中被销毁。

**这种规则不仅适用于if语句块,而且适用于所有的代码块。这是一种被称为“块作用域”的规则。这就是说,一旦你在一个块中(用大括号 { } 定义的代码区域)声明了一个变量,那么这个变量只在该块及其子块中存在。**出了这个块,这个变量就不存在了。

这种规则有助于管理内存和避免变量名冲突。对于更大的程序,正确管理变量的作用域是非常重要的,可以避免很多错误。

应该修改为:

    //左数组,左闭右开
	vector<int>left;
    if(nums.begin()!=nums.begin()+maxIndex){
        left = vector<int>(nums.begin(),nums.begin()+maxIndex);
    }
    //右数组,左闭右开
	vector<int>right;
    if(nums.maxIndex+1!=nums.end()){
        right = vector<int>(nums.begin()+maxIndex+1,nums.end());
    }

修改也要注意left和right的赋值方式,if里面绝对不能重新声明vectorleft = (),这样的话left的值仍然是出了if语句块就会被销毁。

如果像最开始的写法一样,if 语句内部重新声明 vector<int> left,这将会创建一个新的 left 向量,这个新的向量仅在 if 语句块的作用域内有效,在 if 语句块外部,原来的 left 向量并没有被改变

修改后的完整版

class Solution {
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
    //终止条件:数组内没有没遍历到的元素了,也就是切出来的左区间和右区间都是NULL
    //条件是if(nums.empty())的话,就不需要判断left和right是不是空的,是否存在操作空数组的问题了。
    if(nums.empty()){
        return nullptr; //返回空指针
    }
    //找最大值及其下标
    int maxValue=INT_MIN;
    int index;
    int maxIndex;
    for(index=0;index<nums.size();index++){
        if(nums[index]>maxValue){
            maxValue = nums[index];//最大值
            maxIndex = index;//最大值的下标
        }
    }
    //找到最大值之后,根节点数值确定
    TreeNode* root = new TreeNode(maxValue);
    //nums.erase(nums[maxIndex]);
    
    //分割数组
    //左数组,左闭右开
    vector<int>left = vector<int>(nums.begin(),nums.begin()+maxIndex);
    //分割数组后进行左右子树的递归
    //这种写法不需要判断是不是操作空数组left,因为终止条件会判定
    root->left = constructMaximumBinaryTree(left);
    
    //右数组,左闭右开
    vector<int>right = vector<int>(nums.begin()+maxIndex+1,nums.end());
    root->right = constructMaximumBinaryTree(right);
    
    return root;

    }
};

递归进一步理解

当我们在递归调用 constructMaximumBinaryTree(left)时,新的 left 数组会传递给 constructMaximumBinaryTree 函数作为它的 nums 参数,并代替nums执行nums的所有操作

也就是说,发生递归的时候,当我们传入constructMaximumBinaryTree(left)的时候,函数最开始的if(nums.empty()) 判断,nums会自动换成left。递归函数会对这个新的 nums 数组执行与前一次递归调用相同的操作

这就是递归的本质——你在调用同一个函数,但每次调用时传递的参数可能不同,然后这个函数会对传递进来的参数执行相同的操作。

关于终止条件

因为这道题力扣上的要求里,写了nums不为空,因此本题终止条件实际上也可以写成

if(nums.size()==1){
    return root(nums[0]);//直接构造根节点返回
}

但是,这种写法,在递归传入left之前,必须先判断left是不是空,否则就会涉及到操作空数组的问题,因为这种终止条件写法,并没有在最前面判空并返回的操作。就会涉及到操作空数组问题。空数组报错问题详见上一篇博客。(1条消息) Char 45: runtime error: applying non-zero offset 131060 to null pointer (stl_iterator. h)_大磕学家ZYX的博客-CSDN博客

优化

这种写法还是比较冗余的,并且效率很低。为什么效率低,因为每次分割nums数组的时候,都构造了一个新数组构造新数组本来就是很耗时的

代码的优化就是不构造新数组,直接在原有数组nums的基础上进行切割,也就是直接操作下标。每次传入参数的时候,直接传入nums里面左右区间的下标就可以了

优化写法如下:

  • 由于优化之后的写法,在传入nums数组的同时还要传入左区间和右区间,因此需要单独把函数拿出来,以便于修改增加函数参数
class Solution {
private:
    // 在左闭右开区间[left, right),构造二叉树
    TreeNode* traversal(vector<int>& nums, int left, int right) {
        if (left >= right) return nullptr;

        // 分割点下标:maxValueIndex
        int maxValueIndex = left;
        for (int i = left + 1; i < right; ++i) {
            if (nums[i] > nums[maxValueIndex]) maxValueIndex = i;
        }

        TreeNode* root = new TreeNode(nums[maxValueIndex]);

        // 左闭右开:[left, maxValueIndex)
        root->left = traversal(nums, left, maxValueIndex);

        // 左闭右开:[maxValueIndex + 1, right)
        root->right = traversal(nums, maxValueIndex + 1, right);

        return root;
    }
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return traversal(nums, 0, nums.size());
    }
};

时间复杂度和空间复杂度的优化

  1. 空间复杂度:该方法不再创建新的数组副本,而是通过传递下标,直接在原数组上进行操作,大大减少了空间的使用。这种优化使得空间复杂度从O(nlogn)降低到了O(n),其中n为数组长度。这在数组非常大的情况下会有显著的性能提升。

    原空间复杂度是O(nlogn)是因为每一层递归都要创建新数组,当树元素数目是n的时候,树的高度是logn

  2. 时间复杂度:虽然该方法并没有显著地降低时间复杂度,但由于避免了新数组的创建和删除,减少了大量的内存操作,因此在实际运行时,可能会比创建新数组的版本快一些。

总的来说,这种写法的主要优化在于空间复杂度的降低和内存操作的减少,这使得算法更加高效。但是,无论采用哪种方式,时间复杂度的主要部分仍然来自于查找最大值,这部分的时间复杂度为O(n),所以总体的时间复杂度仍然是O(n^2),其中n为数组长度。

补充:二叉树的高度logn

对于一个完全平衡的二叉树(每个节点的左右子树的高度差不超过1),如果树有n个节点,那么它的高度大约是log(n)。这是因为每增加一层,节点的数量大约会翻倍。对于这样的二叉树,任何一个节点(代表数组中的一个元素)都在从根到叶子的路径上,而这条路径的长度就是树的高度,大约是log(n)

原先的做法,每次递归都需要创建新的数组,这样每个元素最多会被复制 log(n) 次(在平衡树的情况下),所以空间复杂度是 O(nlogn)

而在优化后的做法中,由于我们直接在原数组上进行操作,并没有创建新的数组,因此避免了数组的复制。这种情况下,主要的空间消耗来自于递归调用的栈空间。在最坏的情况下(即树退化为链表,递归深度达到 n),空间复杂度是 O(n)。在最好的情况下(即树完全平衡,递归深度为 log(n)),空间复杂度是 O(logn)。

617.合并二叉树

给你两棵二叉树: root1 和 root2 。

想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。

返回合并后的二叉树。

注意: 合并过程必须从两个树的根节点开始。
在这里插入图片描述
输入:root1 = [1,3,2,5], root2 = [2,1,3,null,4,null,7]
输出:[3,4,5,5,4,null,7]

示例 2:
输入:root1 = [1], root2 = [1,2]
输出:[2,2]

思路

这道题比较难的点在于一起操作两个二叉树。遍历顺序方面,因为也是构造一棵新的二叉树,所以选择前序遍历。前序遍历中左右,先合并根节点,再处理左右节点。

完整版

  • 这种写法为降低空间复杂度,直接改tree1
TreeNode* mergeTrees(TreeNode* root1,TreeNode* root2){
    //终止条件
    //如果tree1这个节点是空的,就返回tree2对应位置的节点
    if(root1==NULL){
        return root2;  //tree1和tree2的遍历逻辑是同步进行遍历,终止条件可以先写上
    }
    if(root2==NULL){
        return root1; //这里如果tree1和tree2同时为空,就是直接return NULL,并不影响,已经包含了
    }
    
    //单层递归,前序遍历
    //降低空间复杂度,我们直接改tree1,不重新定义
    //两个节点都存在
    root1->val+=root2->val;
    root1->left = mergeTrees(root1->left,root2->left);
    root1->right = mergeTrees(root1->right,root2->right);
    return root1; 
}

定义新二叉树的写法

  • 如果tree1和tree2不能修改,我们就定义新的二叉树来存放合并结果
TreeNode* mergeTrees(TreeNode* root1,TreeNode* root2){
    //终止条件不变
    //如果tree1这个节点是空的,就返回tree2对应位置的节点
    if(root1==NULL){
        return root2;  //tree1和tree2的遍历逻辑是同步进行遍历,终止条件可以先写上
    }
    if(root2==NULL){
        return root1; //这里如果tree1和tree2同时为空,就是直接return NULL,并不影响,已经包含了
    }
    
    //单层递归,前序遍历
    //定义新的树节点
    TreeNode* newTree = new TreeNode;
    newTree->val = root1->val+root2->val;
    newTree->left = mergeTrees(root1->left,root2->left);
    newTree->right = mergeTrees(root1->right,root2->right);
    return newTree; 
}

此时开辟了新的空间,空间复杂度是O(n),n是合并后新二叉树的节点个数。

101.对称二叉树 实际上也是操作两个二叉树,对称二叉树是对比根节点左子树和右子树是否可以相互翻转。可以结合本题一起来看。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/640593.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Gitlab CI/CD概述

前言 CI/CD 是一种持续开发软件的方法&#xff0c;可以不断的进行构建、测试和部署代码迭代更改。这种迭代有助于减少基于错误或失败的版本进行开发新代码的可能性。使用这种方法&#xff0c;从新代码开发到部署&#xff0c;可以减少人工干预甚至不用干预。 达到持续的方法主要…

Python实例属性和实例方法_类对象、类属性、类方法、静态方法

一、实例属性 实例属性是从属于实例对象的属性&#xff0c;也称为“实例变量”。他的使用有如下几个要点&#xff1a; 实例属性一般在__init__()方法中通过如下代码定义&#xff1a; self.实例属性名 初始值 在本类的其他实例方法中&#xff0c;也是通过self进行访问&#x…

QGIS下载天地图瓦片数据

说明 介绍利用QGIS下载天地图瓦片数据。 关键字:window、QGIS、天地图 环境准备 QGIS版本为3.28.3 基本步骤 一、在Browser面板中找到XYZ Tiles 二、New Connection 参数设置 天地图参数 name:如"天地图影像"url :https://t5.tianditu.gov.cn/DataServer?T=…

U-Mail邮件系统:严防数据泄露 保障企业数据安全

在数字经济时代&#xff0c;数据已经成为企业的核心生产要素&#xff0c;也由此滋生了牟取暴利的黑色产业链&#xff0c;企业数据泄露事件有增无减。根据IdentifyTheft Research Center中心的数据显示&#xff0c;2022年世界范围内的数据泄露事件比2021年增长了14%。其中&#…

chatgpt赋能python:Python如何得出结果:从基础语法到高级算法

Python如何得出结果&#xff1a;从基础语法到高级算法 作为一种流行的编程语言&#xff0c;Python被广泛应用于数据分析、人工智能、Web开发等领域。但是&#xff0c;Python也是一种非常值得学习的SEO工具&#xff0c;它可以帮助你得出有关网站排名、竞争对手分析、关键词选择…

JavaScript-Vue

2 Vue 2.1 Vue概述 通过我们学习的htmlcssjs已经能够开发美观的页面了&#xff0c;但是开发的效率还有待提高&#xff0c;那么如何提高呢&#xff1f;我们先来分析下页面的组成。一个完整的html页面包括了视图和数据&#xff0c;数据是通过请求 从后台获取的&#xff0c;那么…

游览器获取用户位置信息,不同游览器获取位置信息不一致

问题 游览器获取用户位置信息&#xff0c;不同游览器获取位置信息不一致 详细问题 对于下述代码 <!DOCTYPE html> <html> <head><title>获取用户经纬度</title><script>function getLocation() {if (navigator.geolocation) {navigato…

数据库原理

做应用开发的同学常常觉得数据库由DBA运维&#xff0c;自己会写SQL就可以了&#xff0c;数据库原理不需要学习。其实即使是写SQL也需要了解数据库原理&#xff0c;比如我们都知道&#xff0c;SQL的查询条件尽量包含索引字段&#xff0c;但是为什么呢&#xff1f;这样做有什么好…

第三章Java锁—基础

文章目录 乐观锁和悲观锁悲观锁悲观锁的实现方式 乐观锁乐观锁的实现方式版本号实现的大致流程 8锁案例弄清synchronized锁了什么3个体现同步方法和同步块&#xff0c;哪个是更好的选择 字节码角度分析synchronized实现文件反编译技巧synchronized同步代码块synchronized普通同…

pikachu靶场-Over Permission

Over Permission&#xff08;越权&#xff09; 用户A 的权限小于用户B 的权限&#xff0c;此时用用户 A 的权限去操作用户 B 的数据&#xff0c;如果能够操作成功&#xff0c;就称之为越权操作。 越权漏洞一般容易出现在权限页面&#xff08;需要登录的页面&#xff09;增、删…

文件系统原理

文件及硬盘管理是计算机操作系统的重要组成部分&#xff0c;让微软走上成功之路的正是微软最早推出的个人电脑PC操作系统&#xff0c;这个操作系统就叫DOS&#xff0c;即Disk Operating System&#xff0c;硬盘操作系统。我们每天使用电脑都离不开硬盘&#xff0c;硬盘既有大小…

黑龙江二造开始报名!文件指出建设单位对现场人员配备达标负首责

注意&#xff01;2023年黑龙江二级造价考试报名时间公布&#xff01;&#xff01;&#xff01;可以开始报名&#xff01; ✨考试时间&#xff1a;7月9日 ✨报名时间&#xff1a;6月6日—6月14日 ✨缴费时间&#xff1a;6月6日—6月16日 ✨打印准考证时间&#xff1a;7月4日&am…

程序运行原理

程序是如何运行起来的 软件被开发出来&#xff0c;是文本格式的代码&#xff0c;这些代码通常不能直接运行&#xff0c;需要使用编译器编译成操作系统或者虚拟机可以运行的代码&#xff0c;即可执行代码&#xff0c;它们都被存储在文件系统中。不管是文本格式的代码还是可执行…

ChatGPT付费创作系统V2.0.2独立版+小程序安装教程

ChatGPT付费创作系统V2.0.2独立版播播资源测试了下相比&#xff0c;本版核心WEB端进行升级优化&#xff0c;前端增加了创作、模拟、使用帮助等选项&#xff0c;小程序端相比上一版无大的变化。体验下来问答速度感觉体验更好。小程序端有更新请对应开发工具更新上传&#xff0c;…

HAL库记录-SDRAM的使用

正点原子--阿波罗开发板STM32F429IGT6 CLK 时钟信号&#xff0c;在该时钟的上升沿采集输入信号 CKE 时钟使能&#xff0c;禁止时钟时&#xff0c;SDRAM 会进入自刷新模式 CS# 片选信号&#xff0c;低电平有效 RAS# 行地址选通信号&#xff…

剑指offer57.和为s的两个数字

双指针i从左往右&#xff0c;j从右往左&#xff0c;如果大于目标值&#xff0c;j往左走否则i往右走&#xff0c;直到等于目标值 class Solution {public int[] twoSum(int[] nums, int target) {int[] res new int[2];int i 0; int j nums.length-1;int sum nums[i] nums…

docker指令

镜像命令 镜像命令 命令描述docker images php查看名字为php的镜像docker images查看所有镜像docker search搜索镜像docker pull 镜像名:tag 拉取镜像docker rmi php删除php镜像 (保存php镜像到目录) docker image save php > D:\phpstudy_pro\WWW\docker\php.tgz 或者 …

【利用AI让知识体系化】简要了解面向对象编程设计

文章目录 I. 面向对象编程简介面向对象编程的定义与发展历程面向对象编程的优点和特点面向对象和面向过程和面向函数式编程之间的对比 II. 面向对象编程的基本概念类和对象抽象和封装继承和多态封装、继承和多态之间的一些对比 III. 面向对象编程设计原则单一职责原则&#xff…

如何搭建高可用redis架构?

题记 Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库&#xff0c;并提供多种语言的 API。 如今&#xff0c;互联网业务的数据正以更快的速度在增长&#xff0c;数据类型越来越丰富&#xff0c;这对数据处理的速度和能力提…

chatgpt赋能python:Python怎么建网站的SEO

Python怎么建网站的SEO 介绍 Python是一种流行的编程语言&#xff0c;其灵活性和可靠性使其成为了许多网站开发人员的首选语言之一。Python可以用于构建各种类型的网站&#xff0c;包括电子商务和企业级应用程序等。但是&#xff0c;构建一个网站是不够的&#xff0c;您还需要…