Java版LeetCode热题100之柱状图中最大的矩形:单调栈深度解析与实战指南

Java版LeetCode热题100之柱状图中最大的矩形:单调栈深度解析与实战指南

本文将全面剖析 LeetCode 热题第84题《柱状图中最大的矩形》,从题目理解、暴力解法、单调栈优化(双次遍历 vs 单次遍历),到代码实现、复杂度分析、面试技巧、实际应用场景,层层递进,帮助你彻底掌握这一经典算法问题。


一、原题回顾

题目描述:

给定n个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例:

示例 1:

输入:heights = [2,1,5,6,2,3] 输出:10 解释:最大的矩形为图中红色区域,面积为 10

图示说明

  • 柱子高度:[2, 1, 5, 6, 2, 3]
  • 最大矩形由第2~3根柱子(高度5和6)构成,以高度5为基准,宽度为2,面积=5×2=10;
  • 或由第2~5根柱子(高度5,6,2,3)构成,以高度2为基准,宽度为4,面积=2×4=8(非最大)。

示例 2:

输入:heights = [2,4] 输出:4

解释:以高度2为基准,宽度2,面积=4;或以高度4为基准,宽度1,面积=4。

提示:

  • 1≤heights.length≤1051 \leq \text{heights.length} \leq 10^51heights.length105
  • 0≤heights[i]≤1040 \leq \text{heights}[i] \leq 10^40heights[i]104

二、原题分析

2.1 问题本质

我们需要在柱状图中找到一个连续区间[L, R],使得该区间内所有柱子的最小高度h宽度w = R - L + 1的乘积h × w最大。

这等价于:对每个可能的高度h,找到它能向左右扩展的最大宽度

2.2 暴力解法及其局限性

方法1:枚举宽度(两重循环)
  • 枚举所有可能的左右边界[L, R]
  • 计算区间最小高度minH
  • 面积 =(R - L + 1) * minH

时间复杂度O(n2)O(n^2)O(n2)
问题:当n=105n = 10^5n=105时,操作次数达5×1095 \times 10^95×109,必然超时。

方法2:枚举高度(一重循环 + 双指针)
  • 对每个柱子i,以其高度h = heights[i]为矩形高;
  • 向左/右扩展,直到遇到高度< h的柱子;
  • 宽度 =right - left + 1,面积 =h × width

时间复杂度:最坏O(n2)O(n^2)O(n2)(如递增序列)
问题:仍无法通过大数据测试。

💡关键洞察:两种暴力法都因重复计算而低效。我们需要一种方法,对每个柱子,快速找到其左右边界


三、答案构思

3.1 核心思想:单调栈

对于每个柱子i,我们需要找到:

  • 左边界:左侧最近的、高度< heights[i]的柱子下标L
  • 右边界:右侧最近的、高度< heights[i]的柱子下标R
  • 则以heights[i]为高的最大矩形宽度 =R - L - 1

为什么是“小于”而非“小于等于”?

  • 若用“小于等于”,可能导致重复计算或边界错误;
  • 但后续会看到,单次遍历版本可用“小于等于”,不影响最终结果。

3.2 方法一:两次遍历单调栈

  • 第一次遍历(从左到右):求每个柱子的左边界;
  • 第二次遍历(从右到左):求每个柱子的右边界;
  • 最后计算最大面积

3.3 方法二:一次遍历单调栈(优化)

  • 在出栈时确定右边界
  • 入栈时确定左边界
  • 只需一次遍历,更高效。

四、完整答案(Java实现)

4.1 方法一:两次遍历单调栈

importjava.util.ArrayDeque;importjava.util.Deque;classSolution{publicintlargestRectangleArea(int[]heights){intn=heights.length;int[]left=newint[n];// left[i] = i左侧最近<heights[i]的下标int[]right=newint[n];// right[i] = i右侧最近<heights[i]的下标Deque<Integer>stack=newArrayDeque<>();// 从左到右:求左边界for(inti=0;i<n;i++){// 弹出所有 >= heights[i] 的元素while(!stack.isEmpty()&&heights[stack.peek()]>=heights[i]){stack.pop();}// 栈空 → 左侧无更小,设为-1(哨兵)left[i]=stack.isEmpty()?-1:stack.peek();stack.push(i);}stack.clear();// 从右到左:求右边界for(inti=n-1;i>=0;i--){while(!stack.isEmpty()&&heights[stack.peek()]>=heights[i]){stack.pop();}// 栈空 → 右侧无更小,设为n(哨兵)right[i]=stack.isEmpty()?n:stack.peek();stack.push(i);}// 计算最大面积intmaxArea=0;for(inti=0;i<n;i++){intwidth=right[i]-left[i]-1;maxArea=Math.max(maxArea,width*heights[i]);}returnmaxArea;}}

4.2 方法二:一次遍历单调栈(推荐)

importjava.util.ArrayDeque;importjava.util.Arrays;importjava.util.Deque;classSolution{publicintlargestRectangleArea(int[]heights){intn=heights.length;int[]left=newint[n];int[]right=newint[n];Arrays.fill(right,n);// 初始化右边界为n(哨兵)Deque<Integer>stack=newArrayDeque<>();for(inti=0;i<n;i++){// 当前高度 <= 栈顶高度 → 栈顶的右边界就是iwhile(!stack.isEmpty()&&heights[stack.peek()]>=heights[i]){right[stack.peek()]=i;// 确定右边界stack.pop();}// 栈空 → 左边界为-1;否则为栈顶left[i]=stack.isEmpty()?-1:stack.peek();stack.push(i);}// 计算最大面积intmaxArea=0;for(inti=0;i<n;i++){intwidth=right[i]-left[i]-1;maxArea=Math.max(maxArea,width*heights[i]);}returnmaxArea;}}

✅ 两种方法均正确,方法二更优(一次遍历,常数更小)。


五、代码分析

5.1 关键概念:哨兵(Sentinel)

  • 左哨兵:位置-1,高度视为-∞
  • 右哨兵:位置n,高度视为-∞
  • 作用:简化边界判断,避免额外 if 条件。

5.2 单调栈的维护

  • 栈存储下标:便于访问高度和计算宽度;
  • 单调递增:从栈底到栈顶,高度严格递增;
  • 弹出条件heights[stack.peek()] >= heights[i]

5.3 示例执行过程(以[2,1,5,6,2,3]为例)

方法二(一次遍历):
iheightstack (下标→高度)操作left[i]right 更新
02[] → [0(2)]入栈-1-
11[0(2)] → [] → [1(1)]2≥1 → pop(0), right[0]=1-1right[0]=1
25[1(1)] → [1(1),2(5)]入栈1-
36→ [1(1),2(5),3(6)]入栈2-
42弹出3→right[3]=4;弹出2→right[2]=4;入栈41right[3]=4, right[2]=4
53→ [1(1),4(2),5(3)]入栈4-

最终边界

  • left = [-1, -1, 1, 2, 1, 4]
  • right = [1, 6, 4, 4, 6, 6] (未更新的保持为6)

面积计算

  • i=0: (1 - (-1) -1) * 2 = 1*2=2
  • i=1: (6 - (-1) -1) * 1 = 6*1=6
  • i=2: (4 - 1 -1) * 5 = 2*5=10 ✅
  • i=3: (4 - 2 -1) * 6 = 1*6=6
  • i=4: (6 - 1 -1) * 2 = 4*2=8
  • i=5: (6 - 4 -1) * 3 = 1*3=3

最大面积 = 10


六、时间复杂度与空间复杂度分析

6.1 时间复杂度

  • 每个下标最多入栈、出栈各一次
  • 总操作数 ≤2n2n2n
  • 时间复杂度:O(n)O(n)O(n)

💡摊还分析(Amortized Analysis):虽然内层有while循环,但整体线性。

6.2 空间复杂度

  • left/right 数组O(n)O(n)O(n)
  • :最坏O(n)O(n)O(n)(如递增序列);
  • 总计:O(n)O(n)O(n)

七、常见问题解答(FAQ)

Q1:为什么弹出条件是>=而不是>

:若用>,则相同高度的柱子不会被弹出,导致右边界计算错误。
但用>=时,最右侧的相同高度柱子会得到正确的右边界,而左侧的会被覆盖,不影响最大值。

Q2:能否不用 left/right 数组,直接计算面积?

:可以!在出栈时直接计算面积:

while(!stack.isEmpty()&&heights[i]<=heights[stack.peek()]){inth=heights[stack.pop()];intw=stack.isEmpty()?i:i-stack.peek()-1;maxArea=Math.max(maxArea,h*w);}

这是更精简的写法(见后文优化)。

Q3:哨兵的作用是什么?

:避免边界判断。例如,若不用左哨兵,需额外判断stack.isEmpty()才能计算宽度。


八、优化思路

8.1 优化1:省略 left/right 数组

publicintlargestRectangleArea(int[]heights){Deque<Integer>stack=newArrayDeque<>();intmaxArea=0;for(inti=0;i<=heights.length;i++){// 添加右哨兵:i == n 时 height = 0inth=(i==heights.length)?0:heights[i];while(!stack.isEmpty()&&h<=heights[stack.peek()]){intheight=heights[stack.pop()];intwidth=stack.isEmpty()?i:i-stack.peek()-1;maxArea=Math.max(maxArea,height*width);}stack.push(i);}returnmaxArea;}
  • 优点:代码更短,空间略省;
  • 技巧:在末尾添加虚拟哨兵(高度0),确保所有元素出栈。

8.2 优化2:使用数组栈

publicintlargestRectangleArea(int[]heights){intn=heights.length;int[]stack=newint[n+1];inttop=-1;intmaxArea=0;for(inti=0;i<=n;i++){inth=(i==n)?0:heights[i];while(top>=0&&h<=heights[stack[top]]){intheight=heights[stack[top--]];intwidth=(top==-1)?i:i-stack[top]-1;maxArea=Math.max(maxArea,height*width);}stack[++top]=i;}returnmaxArea;}
  • 适用场景:性能敏感系统(避免对象开销)。

九、数据结构与算法基础知识点回顾

9.1 单调栈(Monotonic Stack)

  • 定义:栈中元素保持单调性(递增或递减);
  • 类型
    • 单调递增栈:用于找“下一个更小元素”;
    • 单调递减栈:用于找“下一个更大元素”;
  • 本题单调递增栈(因找“下一个更小元素”作为边界)。

9.2 哨兵技巧(Sentinel)

  • 目的:简化边界条件;
  • 应用
    • 数组首尾添加虚拟元素;
    • 链表头结点;
    • 字符串匹配中的终止符。

9.3 摊还分析(Amortized Analysis)

  • 场景:某些操作代价高,但总代价可控;
  • 例子
    • 动态数组扩容;
    • 单调栈的弹出操作;
  • 结论:单次操作均摊O(1)O(1)O(1)

9.4 算法范式:分治 vs 扫描线

  • 分治:可解(如线段树),但复杂;
  • 扫描线 + 单调栈:最优解,直观高效。

十、面试官提问环节(模拟)

Q1:你的解法时间复杂度真的是 O(n) 吗?内层 while 不是可能 O(n) 吗?

:是的。虽然单次while可能弹出多个元素,但每个下标最多被弹出一次。总弹出次数 ≤nnn,故摊还时间复杂度为O(n)O(n)O(n)

Q2:如果柱子高度可以为负数,算法还适用吗?

:不适用。因为矩形面积不能为负,需先过滤非负高度,或重新定义问题。

Q3:能否用动态规划解决?

:可以,但状态转移复杂。例如:

  • left[i] = left[i-1]ifheights[i] >= heights[i-1]else …
    但不如单调栈直观高效。

Q4:这个算法能扩展到二维吗?

:可以!LeetCode 85. 最大矩形就是二维扩展,对每行用本题算法。

Q5:为什么不用线段树?

:线段树可解(查询区间最小值 + 分治),但:

  • 代码复杂;
  • 常数大;
  • 单调栈更优。

十一、这道算法题在实际开发中的应用

11.1 图像处理:最大白色矩形

  • 场景:二值图像中找最大全白矩形;
  • 方法
    1. 对每行计算“向上连续白色像素数” → 得到柱状图;
    2. 对每行应用本题算法;
    3. 取全局最大。
  • 应用:文档扫描、车牌识别。

11.2 数据库:最大连续满足条件的记录

  • 场景:日志中找最长连续时间段,满足“错误率 < 阈值”;
  • 转换:将满足条件的天标记为1,否则0;
  • 问题:找最大全1矩形 → 本题变种。

11.3 资源调度:最大连续可用资源块

  • 场景:内存/磁盘分配中,找最大连续空闲块;
  • 模型:空闲=1,占用=0 → 柱状图高度=连续空闲长度;
  • 算法:本题直接应用。

11.4 游戏开发:地形生成

  • 场景:2D平台游戏中,生成最大平坦区域;
  • 输入:地形高度数组;
  • 输出:最大可建造矩形平台。

11.5 金融风控:最大连续盈利周期

  • 场景:股票收益序列为[+2%, -1%, +3%, +4%, -2%]
  • 转换:盈利=1,亏损=0;
  • 问题:找最大连续盈利矩形(高度=1,宽度=天数)。

💡本质任何需要“在序列中找最大连续满足条件区域”的问题,都可转化为柱状图最大矩形。


十二、相关题目推荐

题号题目关联点
[84]柱状图中最大的矩形本题
[85]最大矩形二维扩展
[42]接雨水单调栈/双指针
[739]每日温度单调栈(下一个更大元素)
[496]下一个更大元素 I单调栈基础
[907]子数组的最小值之和单调栈 + 贡献法
[1856]子数组最小乘积的最大值本题变种

🔔重点练习

  • LeetCode 85:对每行构建柱状图,调用本题算法;
  • LeetCode 907:计算每个元素作为最小值的贡献,需左右边界。

十三、总结与延伸

13.1 核心总结

  • 柱状图最大矩形是单调栈的经典应用
  • 关键:对每个柱子,快速找到左右边界;
  • 最优解一次遍历单调栈,时间O(n)O(n)O(n),空间O(n)O(n)O(n)
  • 技巧:哨兵简化边界,>=处理相等情况。

13.2 延伸思考

  • 变种1:求所有矩形面积之和?
    • 答:计算每个柱子的贡献(作为最小值的区间数)。
  • 变种2:支持动态修改高度?
    • 答:需线段树维护区间最小值 + 分治。
  • 变种3:圆形柱状图(首尾相连)?
    • 答:复制数组或特殊处理跨越边界的矩形。

13.3 学习建议

  1. 手写单调栈模板:形成肌肉记忆;
  2. 画栈变化图:理解弹出逻辑;
  3. 对比暴力解:体会优化价值;
  4. 刷相关题:巩固“边界查找”模式。

🌟最后寄语:单调栈虽小,却是解决序列问题的利器。掌握《柱状图中最大的矩形》,你就掌握了处理“边界查找”类问题的核心思想。算法之美,在于用简洁结构解决复杂问题。继续前行,你离高手又近了一步!


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

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

相关文章

【波束成形】双功能雷达与通信系统Matlab仿真

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;擅长数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。 &#x1f34e; 往期回顾关注个人主页&#xff1a;Matlab科研工作室 &#x1f447; 关注我领取海量matlab电子书和数学建模资料 &#…

在宁波选择研究生留学机构,如何挑选top10确保无隐形消费

在宁波选择研究生留学机构,如何挑选top10确保无隐形消费一、宁波学子如何规避留学中介消费陷阱,科学筛选头部机构?撰写本文时,时间是2026年1月9日。不少宁波高校的学子在筹划研究生留学时,常面临几个核心困扰:网…

99%的Python开发者踩过的坑:浅拷贝与深拷贝的5大误解,你中招了吗?

第一章&#xff1a;99%的Python开发者踩过的坑&#xff1a;浅拷贝与深拷贝的5大误解&#xff0c;你中招了吗&#xff1f;在Python开发中&#xff0c;对象的复制看似简单&#xff0c;实则暗藏玄机。许多开发者误以为赋值操作就是“复制”&#xff0c;殊不知这往往只是创建了引用…

揭秘Python随机数生成器:5个你必须知道的实用技巧

第一章&#xff1a;Python随机数生成器的核心机制Python 的随机数生成能力主要由内置的 random 模块提供&#xff0c;其底层依赖于梅森旋转算法&#xff08;Mersenne Twister&#xff09;。该算法是一种伪随机数生成器&#xff08;PRNG&#xff09;&#xff0c;具有极长的周期&…

聊聊 C++ 中那些容易踩坑的运算符

C++ 里的 :: . < << this this-> 各自是什么、怎么用、底层原理? 这几个关键符号到底干嘛的? :: —— 作用域解析运算符(scope resolution) 作用:告诉编译器“我要用的是某个作用域里的名称”。 常见…

UE5 C++(42):创建 timeLine 时间轴

&#xff08;214&#xff09; &#xff08;215&#xff09; 谢谢

郑州top10研究生留学中介推荐,稳定可靠保障您的留学选择

郑州top10研究生留学中介推荐,稳定可靠保障您的留学选择一、郑州学子如何筛选可靠的研究生留学中介?在郑州市,每年都有大量本科毕业生计划赴海外深造。面对市面上众多的留学服务机构,许多同学与家长常常感到困惑。…

快速落地 JT/T 808 服务端:jt-framework

快速落地 JT/T 808 服务端:jt-framework 快速落地 JT/T 808 服务端:jt-framework 一、项目名称 jt-framework 一句话简介:基于 Spring Boot 的 JT/T 808(并扩展 JT/T 1078、附件服务器、Dashboard)服务端框架,提…

【高薪程序员必会知识点】:深拷贝 vs 浅拷贝——3个实战案例带你彻底掌握

第一章&#xff1a;深拷贝与浅拷贝的核心概念解析在编程中&#xff0c;对象和数据结构的复制操作看似简单&#xff0c;实则暗藏玄机。当一个变量引用复杂数据类型&#xff08;如对象、数组&#xff09;时&#xff0c;直接赋值往往不会创建新的独立副本&#xff0c;而是产生指向…

Python批量处理Word文档:告别重复劳动的3个核心技巧

第一章&#xff1a;Python自动化办公与Word处理概述在现代办公环境中&#xff0c;文档处理占据了大量重复性工作时间。利用Python进行自动化办公&#xff0c;尤其是对Microsoft Word文档的批量生成、修改与格式化操作&#xff0c;已成为提升效率的重要手段。通过python-docx等第…

2026年广州诚信的汽配加盟商城,汽车配件加盟,连锁汽配加盟厂家综合实力参考

引言在 2026 年的广州,汽配加盟行业呈现出蓬勃发展的态势。为了给广大投资者提供客观、公正、真实的汽配加盟参考,我们依据相关权威数据和科学的测评方法,对众多汽配加盟商城、汽车配件加盟品牌以及连锁汽配加盟厂家…

20260121人工智能作业v2

文章目录一、核心命令清单&#xff08;逐条执行&#xff0c;需root权限&#xff09;1. 校验并创建用户组 dev_team2. 创建用户 alice&#xff08;指定主组安全配置&#xff09;3. 创建用户 bob&#xff08;同alice&#xff0c;仅用户名不同&#xff09;4. 创建 /opt/project 目…

2025年国内靠谱的假肢公司推荐与深度解析

面对肢体差异,选择一家专业、可靠且充满人文关怀的假肢公司,是开启新生活的关键一步。市场上服务机构众多,但技术水平、服务质量、后续支持参差不齐,用户常面临“价格不透明”、“装配后不适”、“售后服务缺失”等…

专利--授权及花费(发明)

发明专利授权相关费用需分授权登记阶段和授权后年费阶段&#xff0c;以下是 2026 年官方最新标准&#xff08;人民币&#xff0c;无费减&#xff09;&#xff1a; 一、授权登记阶段费用&#xff08;一次性缴纳&#xff09;二、授权后年费&#xff08;每年缴纳&#xff09;三、费…

Python模块导入失败怎么办?(ModuleNotFoundError深度解析与实战修复)

第一章&#xff1a;Python模块导入失败怎么办&#xff1f;&#xff08;ModuleNotFoundError深度解析与实战修复&#xff09;当Python程序运行时提示 ModuleNotFoundError: No module named xxx&#xff0c;说明解释器无法定位指定模块。该错误通常由路径配置不当、虚拟环境错乱…

连接PostgreSQL总是失败?,一文搞定Python与PostgreSQL无缝集成

第一章&#xff1a;连接PostgreSQL总是失败&#xff1f;常见问题与核心原理在开发和运维过程中&#xff0c;连接 PostgreSQL 数据库失败是常见问题。理解其底层通信机制与配置逻辑&#xff0c;有助于快速定位并解决问题。网络与监听配置 PostgreSQL 默认仅监听本地回环地址&…

【Python报错终极指南】:3步快速解决ModuleNotFoundError难题

第一章&#xff1a;Python报错终极指南的核心价值Python作为一门广泛应用于数据科学、Web开发和自动化脚本的语言&#xff0c;其简洁语法背后隐藏着初学者和资深开发者都可能遭遇的复杂错误。掌握Python报错机制的本质&#xff0c;不仅能快速定位问题&#xff0c;还能提升代码健…

揭秘Python操作PostgreSQL数据库:5个步骤快速上手并避免常见陷阱

第一章&#xff1a;Python连接PostgreSQL数据库概述在现代Web开发和数据处理中&#xff0c;Python因其简洁的语法和强大的生态被广泛用于与关系型数据库交互。PostgreSQL作为功能丰富、可靠性高的开源对象-关系型数据库系统&#xff0c;常与Python配合使用&#xff0c;实现高效…

如何用Python将字符串秒变datetime对象?这4个方法最有效

第一章&#xff1a;字符串转datetime对象的核心意义在现代软件开发中&#xff0c;时间数据的处理无处不在。日志分析、用户行为追踪、任务调度等场景均依赖精确的时间解析。然而&#xff0c;原始时间通常以字符串形式存储或传输&#xff0c;如 "2023-10-05 14:30:00"…

还在用random.randint?这7种高级随机数生成方法你必须掌握,告别初级写法

第一章&#xff1a;Python随机数生成的演进与核心概念Python 的随机数生成功能自诞生以来经历了显著演进&#xff0c;从早期基于简单算法的实现发展为如今支持多种分布和加密安全的成熟体系。其核心依赖于伪随机数生成器&#xff08;PRNG&#xff09;&#xff0c;默认使用梅森旋…