leetcode654:最大二叉树(递归与单调栈双解法)

文章目录

  • 一、 题目描述
  • 二、 核心思路:分而治之与递归构造
  • 三、代码实现与深度解析
  • 四、 关键点与复杂度分析
  • 五、拓展解法
    • 单调栈解法
    • 两种解法对比

LeetCode 654. 最大二叉树,【难度:中等;通过率:82.6%】,这道题的描述本身就是一份递归的构造方式,我们的任务就是将这份描述优雅地 翻译成代码;且思路类似 “由中序与前序/后序遍历结果构建二叉树”,参考:

  • leetcode105深度解析:从前序与中序遍历序列构造二叉树
  • leetcode106深度解析:从中序与后序遍历序列构造二叉树

一、 题目描述

给定一个不含重复元素的整数数组 nums。一个以此数组直接递归构建的 最大二叉树 定义如下:

  1. 二叉树的根是数组 nums 中的最大元素
  2. 左子树是通过数组中 最大值左边部分 递归构造出的最大二叉树
  3. 右子树是通过数组中 最大值右边部分 递归构造出的最大二叉树

返回由 nums 构建的 最大二叉树

示例:

示例 1:
示例 1

输入: 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] 构建出的最大二叉树

示例 2:
示例 2

输入: nums = [3,2,1]
输出: [3,null,2,null,1]

二、 核心思路:分而治之与递归构造

题目的定义已经为我们指明了道路——递归。构建整棵树的过程,和构建其任意子树的过程,遵循着完全相同的规则,只是处理的数组范围不同。这就是典型的“分而治之”思想

我们可以将构建过程分解为三步:

  1. 找到根节点:在当前需要处理的数组范围内,找到最大值及其索引。这个最大值就是当前子树的根节点
  2. 构建左子树:对最大值左边的子数组,递归地执行相同的构建过程,并将返回的根节点作为当前根节点的左孩子
  3. 构建右子树:对最大值右边的子数组,递归地执行相同的构建过程,并将返回的根节点作为当前根节点的右孩子

这个过程会一直持续下去,直到需要处理的子数组为空,此时返回 null,递归终止

三、代码实现与深度解析

直观解法就是定义一个辅助递归函数,该函数接收数组和需要处理的索引范围 [left, right] 作为参数

class Solution {public TreeNode constructMaximumBinaryTree(int[] nums) {// 调用辅助递归函数,初始范围为整个数组return build(nums, 0, nums.length - 1);}/*** 辅助递归函数:在指定的数组范围 [left, right] 内构建最大二叉树* @param nums  原始数组* @param left  当前处理范围的左边界(包含)* @param right 当前处理范围的右边界(包含)* @return 构建好的子树的根节点*/private TreeNode build(int[] nums, int left, int right) {// 步骤 1: 定义递归终止条件// 如果左边界大于右边界,说明当前范围为空,无法构建节点if (left > right) {return null;}// 步骤 2: 在当前范围 [left, right] 内找到最大值的索引int maxIndex = -1;int maxValue = Integer.MIN_VALUE;for (int i = left; i <= right; i++) {if (nums[i] > maxValue) {maxValue = nums[i];maxIndex = i;}}// 步骤 3: 创建根节点TreeNode root = new TreeNode(maxValue);// 步骤 4: 递归构建左子树// 左子树的范围是 [left, maxIndex - 1]root.left = build(nums, left, maxIndex - 1);// 步骤 5: 递归构建右子树// 右子树的范围是 [maxIndex + 1, right]root.right = build(nums, maxIndex + 1, right);// 步骤 6: 返回当前构建好的根节点return root;}
}

四、 关键点与复杂度分析

  • 递归函数的定义build(nums, left, right) 的定义非常关键。它清晰地界定了每次递归需要处理的子问题——在 nums 数组的 [left, right] 区间内构建最大二叉树
  • 寻找最大值:在每个递归步骤中,核心操作都是遍历当前子数组以找到最大值。这是主要的耗时部分
  • 时间复杂度:在最坏的情况下(例如,数组是升序或降序排列的),树会退化成一个链表。每次寻找最大值的操作都需要扫描近乎整个子数组,总的时间复杂度为 O(N) + O(N-1) + … + O(1) = O(N²)
  • 空间复杂度:空间开销主要来自递归调用栈。栈的深度取决于树的高度 H。在最坏情况下(退化为链表),树的高度为 N,因此空间复杂度为 O(N)。在平均情况下(树比较平衡),高度为 O(log N),空间复杂度为 O(log N)

五、拓展解法

这道题的朴素递归解法时间复杂度是 O(N²),有没有优化的可能呢?

答案是肯定的。瓶颈在于每次都需要线性扫描来寻找最大值。如果我们能用更高效的方式在数组的一个区间内找到最大值,就能优化性能。例如,使用单调栈可以在 O(N) 的时间内完成整个构建过程。但这需要更复杂的逻辑,因为它将树的构建过程从 “递归分解” 变为了 “迭代构建”,参考代码如下:

单调栈解法

class Solution {public TreeNode constructMaximumBinaryTree(int[] nums) {if (nums == null || nums.length == 0) {return null;}// 使用数组(刷题环境显然需要追求极致性能,别用Stack)模拟栈// 存储 TreeNode 引用// 栈的大小最大为 nums.lengthTreeNode[] stack = new TreeNode[nums.length];int top = -1; // 栈顶指针,初始化为 -1 表示栈空for (int num : nums) {TreeNode node = new TreeNode(num); // 创建当前节点// 步骤 1: 处理栈顶元素 (pop)// 如果栈不为空,且栈顶元素的值小于当前节点的值// 那么栈顶元素应该成为当前节点的左孩子while (top != -1 && stack[top].val < node.val) {node.left = stack[top]; // 栈顶元素成为当前节点的左孩子top--; // 弹出栈顶元素}// 步骤 2: 连接当前节点的右孩子 (peek)// 如果栈不为空,且栈顶元素的值大于当前节点的值// 那么当前节点应该成为栈顶元素的右孩子if (top != -1) {stack[top].right = node; // 当前节点成为栈顶元素的右孩子}// 步骤 3: 当前节点入栈 (push)top++;stack[top] = node;}// 循环结束后,栈底(即 stack[0])的元素就是整棵树的根节点// 因为它是最后一个入栈的,且没有比它更大的元素让它弹出或成为右孩子return stack[0]; }
}

两种解法对比

递归构建 (朴素、直观解法)单调栈 (O(N) 优化解法)
核心思路分而治之:找到当前范围最大值作为根,递归处理左右子数组利用结构:通过维护一个单调递减栈,一次遍历确定每个节点的左右孩子
构建过程1. 遍历当前子数组找最大值
2. 创建根节点
3. 递归构建左子树
4. 递归构建右子树
1. 遍历数组元素,创建节点 curr
2. 栈顶小于 curr,弹出并作为 curr 的左孩子
3. 栈顶大于 currcurr 作为栈顶的右孩子
4. curr 入栈
时间复杂度O(N²):每次递归都可能线性扫描子数组找最大值O(N):每个元素最多入栈一次、出栈一次,哈希表操作平均 O(1)
空间复杂度O(N):递归栈深度最坏为 N (退化为链表)O(N):栈中最多存储 N 个节点 (最坏情况)
代码可读性:与题目定义高度吻合,直观易懂中等/低:逻辑较为抽象,需要理解单调栈的特性和指针连接规则
理解难度较低,适合初学者入门递归较高,需要对栈、树结构和相对大小关系有深入理解
底层实现递归函数调用栈通常使用数组Deque (如 ArrayDeque) 模拟栈;不建议使用Stack
缺点效率低,可能导致超时逻辑复杂,不易一次性写对

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

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

相关文章

Python 循环语法详解

在编程中&#xff0c;循环是一种非常常见的控制结构。很多时候&#xff0c;我们需要重复做一些事情&#xff0c;比如遍历列表、处理数据、尝试直到成功等。这时候&#xff0c;就离不开循环了。Python 提供了两种主要的循环结构&#xff1a;for 循环 和 while 循环。本篇文章会从…

一个小巧神奇的 USB数据线检测仪

一个小巧的数据线检测仪&#xff0c;检测各种USB数据线是否损坏、通断&#xff0c;TYPE_C、MICRO_B、苹果线、烧录线、网线都可检测。嵌入式开发者的称手工具。 这个是我个人制作的&#xff0c;SMT和连接器比较贵&#xff0c;特别是24PIN的C口连接器&#xff0c;我挂在黄色小鱼…

37.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--增加Github Action

在第二部分&#xff08;微服务基础工具与技术&#xff09;中我们讲解了GitHub Action的相关知识&#xff0c;那么在这一节中&#xff0c;我们将为已有的微服务增加GitHub Action的支持。 一、什么是GitHub Action 虽然前面已经介绍过GitHub Action的相关知识&#xff0c;但这里…

ROS2 通过 命令行 发布速度控制指令 控制 麦克娜姆轮

在 ROS2 中&#xff0c;要通过命令行发布速度控制指令来控制麦克娜姆轮机器人&#xff0c;你需要知道机器人所使用的速度控制话题和消息类型。通常麦克娜姆轮机器人使用geometry_msgs/Twist消息类型来接收速度指令。 以下是通过命令行发布速度控制指令的方法&#xff1a; 首先确…

多层Model更新多层ListView

一、总体架构QML (三层 ListView)└─ C 单例 DataCenter (QQmlContext 注册)├─ L1Model (一级节点)│ └─ 内部持有 QList<L2Model*>│ └─ L2Model (二级节点)│ └─ 内部持有 QList<L3Model*>│ └─ L3Model (三级节…

Git基础操作教程

本文目的是掌握Git基础操作教程一、Git简介Git&#xff1a;分布式版本控制系统&#xff0c;使用仓库(Repository)来记录文件的变化最流行的版本控制系统有两种&#xff1a;集中式&#xff08;SVN&#xff09;、分布式&#xff08;Git&#xff09;二、Git操作1.创建仓库仓库(Rep…

Android 之 Kotlin

变量变量的声明Kotlin使用var&#xff0c;val来声明变量&#xff0c;注意&#xff1a;Kotlin不再需要;来结尾var 可变变量&#xff0c;对应java的非final变量var b 1val不可变变量&#xff0c;对应java的final变量val a 1两种变量并未声明类型&#xff0c;这是因为Kotlin存在…

Design Compiler:布图规划探索(ICC)

相关阅读 Design Compilerhttps://blog.csdn.net/weixin_45791458/category_12738116.html?spm1001.2014.3001.5482 简介 在Design Compiler Graphical中&#xff0c;可以用布图规划探索(Floorplan Exploration)功能&#xff0c;打开IC Compiler进行布图规划的创建、修改与分…

《蓝牙低功耗音频技术架构解析》

《2025GAS声学大讲堂—音频产业创新技术公益讲座》低功耗蓝牙音频系列专题LE Audio & Auracast™专题讲座第1讲将于8月7日周四19点开讲&#xff0c;本次邀请了蓝牙技术联盟 技术与市场经理 鲁公羽 演讲&#xff0c;讲座主题&#xff1a;《蓝牙低功耗音频技术架构解析》。&…

ubuntu apt安装与dpkg安装相互之间的关系

0. 问题解释 在linux系统中&#xff0c;使用neofetch命令可以看到现在系统中使用dpkg, flatpak, snap安装的包的数量&#xff0c;那么使用apt安装的包被统计在什么位置了呢&#xff0c;使用apt的安装流程和使用flatpak的安装流程有什么关系和区别呢?1. apt 安装的包在哪里&…

YooAsset源码阅读-Downloader篇

YooAsset源码阅读-Downloader 继续 YooAsset 的 Downloader &#xff0c;本文将详细介绍如何创建下载器相关代码 CreateResourceDownloaderByAll 关键类 PlayModeImpl.csResourceDownloaderOperation.csDownloaderOperation.csBundleInfo.cs CreateResourceDownloaderByAll 方法…

豆包新模型与 PromptPilot 实操体验测评,AI 辅助创作的新范式探索

摘要&#xff1a;在 AI 技术飞速发展的当下&#xff0c;各类大模型及辅助工具层出不穷&#xff0c;为开发者和创作者带来了全新的体验。2025 年 7 月 30 日厦门站的火山方舟线下 Meetup&#xff0c;为我们提供了近距离接触豆包新模型与 PromptPilot 的机会。本次重点体验了实验…

深入探讨AI在测试领域的三大核心应用:自动化测试框架、智能缺陷检测和A/B测试优化,并通过代码示例、流程图和图表详细解析其实现原理和应用场景。

引言随着人工智能技术的飞速发展&#xff0c;软件测试领域正在经历一场深刻的变革。AI技术不仅提高了测试效率&#xff0c;还增强了测试的准确性和覆盖范围。本文将深入探讨AI在测试领域的三大核心应用&#xff1a;自动化测试框架、智能缺陷检测和A/B测试优化&#xff0c;并通过…

音视频学习笔记

0.vs应用其他库配置1基础 1.1视频基础 音视频录制原理音视频播放原理图像表示rgb图像表示yuvhttps://blog.51cto.com/u_7335580/2059670 https://blog.51cto.com/cto521/1944224 https://blog.csdn.net/mandagod/article/details/78605586?locationNum7&fps1 视频主要概念…

LLM隐藏层状态: outputs.hidden_states 是 MLP Residual 还是 Layer Norm

outputs.hidden_states 是 MLP Residual 还是 Layer Norm outputs.hidden_states 既不是单纯的 MLP Residual,也不是单纯的 Layer Norm,而是每一层所有组件(包括 Layer Norm、注意力、MLP、残差连接等)处理后的最终隐藏状态。具体需结合 Transformer 层的结构理解: 1. T…

XML 用途

XML 用途 引言 XML&#xff08;可扩展标记语言&#xff09;是一种用于存储和传输数据的标记语言。自1998年推出以来&#xff0c;XML因其灵活性和可扩展性&#xff0c;在众多领域得到了广泛应用。本文将详细介绍XML的用途&#xff0c;帮助读者全面了解这一重要技术。 一、数据存…

亚马逊撤离Google购物广告:重构流量生态的战略博弈

战略突变&#xff1a;从渐进收缩到全面退潮的背后逻辑亚马逊在2025年7月突然全面停止Google Shopping广告投放&#xff0c;这场看似 abrupt 的决策实则经历了一年多的战略铺垫&#xff0c;从2024年Q1开始的预算削减&#xff0c;到2025年Q2美国市场支出减半&#xff0c;直至核心…

【QT】常⽤控件详解(三)常用按钮控件PushButton RadioButton CheckButton Tool Button

文章目录前言一、PushButton1.1 QAbstractButton1.2 添加图标的按钮1.3 给按钮添加快捷键1.4 代码⽰例:按钮的重复触发二、 RadioButtion2.1简介2.2 几个槽函数 click,press,release, toggled 的区别2.2 模拟分组点餐三、 CheckBox四、Tool Button&#x1f6a9;总结前言 一、P…

数据结构:反转链表(reverse the linked list)

目录 通过交换元素值实现反转&#xff08;reverse by swapping elements&#xff09; 滑动指针&#xff08;sliding pointers&#xff09; 使用滑动指针反转链表&#xff08;Reversing a Linked List using Sliding Pointers&#xff09; 对比分析 如何用递归&#xff08;R…

【C#】基于SharpCompress实现压缩包解压功能

1.SharpCompress安装 在vs的nuget下搜索安装SharpCompress&#xff0c;如图所示2.解压缩包功能实现 /// <summary> /// 解压压缩包 /// </summary> /// <param name"filePath">压缩包文件路径</param> /// <param name"directoryPat…