当前位置: 首页 > news >正文

单调栈算法精解(Java实现):从原理到高频面试题

在算法与数据结构的领域中,单调栈(Monotonic Stack)凭借其独特的设计和高效的求解能力,成为解决特定类型问题的神兵利器。它通过维护栈内元素的单调性,能将许多问题的时间复杂度从暴力解法的\(O(n²)\)优化至\(O(n)\),极大提升算法效率。本文将深入解析单调栈的核心思想、Java 实现方式,并结合 LeetCode 高频面试题,帮助读者透彻掌握这一重要算法工具。

一、单调栈核心思想

单调栈是一种特殊的数据结构,其核心在于保持栈内元素的单调性,这一特性使其在解决 “寻找下一个更大 / 更小元素” 类问题时表现卓越。它具有以下核心特性:

  1. 元素单调性:栈内元素始终保持严格递增或递减顺序,根据具体问题需求构建单调递增栈或单调递减栈。
  2. 后进先出机制:新元素入栈时,需与栈顶元素进行比较,根据单调性规则决定是否触发栈顶元素出栈。
  3. 时空效率优势:通过一次遍历完成操作,时间复杂度为\(O(n)\);空间复杂度为\(O(n)\),用于存储栈内元素 。

二、单调栈两种形式

根据元素单调性的不同,单调栈可分为单调递增栈和单调递减栈,二者在操作规则与应用场景上各有侧重:

类型操作规则典型应用场景适用问题特征
单调递增栈新元素大于或等于栈顶元素时,栈顶元素出栈,直至满足单调性寻找下一个更小元素、计算元素左侧 / 右侧第一个小于自身的元素涉及局部最小值判断、区间边界查找的问题
单调递减栈新元素小于或等于栈顶元素时,栈顶元素出栈,直至满足单调性寻找下一个更大元素、计算元素左侧 / 右侧第一个大于自身的元素涉及局部最大值判断、区间范围统计的问题

三、Java 实现模板

public class MonotonicStack {// 单调递减栈模板,用于寻找下一个更大元素public int[] nextGreaterElement(int[] nums) {int[] res = new int[nums.length];Arrays.fill(res, -1);Deque<Integer> stack = new ArrayDeque<>();for (int i = 0; i < nums.length; i++) {while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {int idx = stack.pop();res[idx] = nums[i];}stack.push(i);}return res;}
}

上述代码展示了使用 Java 实现单调递减栈的核心逻辑:

  • 通过Deque接口创建栈,ArrayDeque作为具体实现类,提供高效的栈操作。
  • 遍历数组过程中,当新元素大于栈顶元素时,弹出栈顶元素并记录其下一个更大元素,最终栈内剩余元素的下一个更大元素为 -1。

四、经典问题解析

4.1 下一个更大元素 I(LeetCode 496)

题目要求:给定两个没有重复元素的数组 nums1 和 nums2 ,其中 nums1 是 nums2 的子集。返回 nums1 中每个元素在 nums2 中的下一个比其大的值。如果不存在,对应位置输出 -1。

class Solution {public int[] nextGreaterElement(int[] nums1, int[] nums2) {Map<Integer, Integer> nextGreater = new HashMap<>();Deque<Integer> stack = new ArrayDeque<>();for (int num : nums2) {while (!stack.isEmpty() && num > stack.peek()) {nextGreater.put(stack.pop(), num);}stack.push(num);}int[] res = new int[nums1.length];for (int i = 0; i < nums1.length; i++) {res[i] = nextGreater.getOrDefault(nums1[i], -1);}return res;}
}

解题思路:

  1. 遍历 nums2 构建单调递减栈,同时使用哈希表记录每个元素的下一个更大元素。
  2. 遍历 nums1 ,通过哈希表快速获取对应元素的下一个更大元素,若不存在则返回 -1。

4.2 每日温度(LeetCode 739)

题目要求:给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

class Solution {public int[] dailyTemperatures(int[] temperatures) {int[] res = new int[temperatures.length];Deque<Integer> stack = new ArrayDeque<>();for (int i = 0; i < temperatures.length; i++) {while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {int idx = stack.pop();res[idx] = i - idx;}stack.push(i);}return res;}
}

解题思路:

  • 利用单调递减栈存储温度的索引,当遇到更高温度时,计算当前温度与栈顶温度索引的差值,即为下一个更高温度的天数。

4.3 柱状图中最大矩形(LeetCode 84)

题目要求:给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。

class Solution {public int largestRectangleArea(int[] heights) {Deque<Integer> stack = new ArrayDeque<>();int maxArea = 0;int[] newHeights = Arrays.copyOf(heights, heights.length + 1);for (int i = 0; i < newHeights.length; i++) {while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {int h = newHeights[stack.pop()];int w = stack.isEmpty() ? i : i - stack.peek() - 1;maxArea = Math.max(maxArea, h * w);}stack.push(i);}return maxArea;}
}

解题思路:

  • 在原始高度数组末尾添加一个 0,作为哨兵节点,确保所有元素都能被处理。
  • 利用单调递增栈,计算每个高度对应的矩形宽度,从而得出最大矩形面积。

五、算法核心技巧

5.1 处理循环数组

// 循环数组解法(LeetCode 503)
public int[] nextGreaterElements(int[] nums) {int n = nums.length;int[] res = new int[n];Arrays.fill(res, -1);Deque<Integer> stack = new ArrayDeque<>();for (int i = 0; i < 2 * n; i++) {int num = nums[i % n];while (!stack.isEmpty() && num > nums[stack.peek()]) {res[stack.pop()] = num;}if (i < n) stack.push(i);}return res;
}

技巧:通过将循环数组视为两倍长度的数组进行遍历,模拟循环效果,解决循环数组中寻找下一个更大元素的问题。

5.2 二维矩阵问题

// 最大矩形(LeetCode 85)
public int maximalRectangle(char[][] matrix) {if (matrix.length == 0) return 0;int[] heights = new int[matrix[0].length];int maxArea = 0;for (char[] row : matrix) {for (int j = 0; j < row.length; j++) {heights[j] = row[j] == '1' ? heights[j] + 1 : 0;}maxArea = Math.max(maxArea, largestRectangleArea(heights));}return maxArea;
}

技巧:将二维矩阵逐行转化为柱状图,通过复用柱状图中最大矩形的求解方法,解决二维矩阵中的最大矩形问题。

六、复杂度分析

问题类型时间复杂度空间复杂度核心操作说明
一维数组\(O(n)\)\(O(n)\)对数组进行单次遍历,栈操作复杂度与遍历次数相关
二维矩阵\(O(mn)\)\(O(n)\)逐行处理矩阵,每行调用一维数组解法,m 为行数,n 为列数
循环数组\(O(n)\)\(O(n)\)虚拟扩展数组长度为两倍,本质仍为单次遍历操作

七、常见错误与调试技巧

7.1 常见错误:

  1. 索引混淆:在栈中存储元素值还是元素索引不明确,导致后续计算错误。
  2. 边界处理:未正确处理栈为空的情况,或在数组边界处的操作逻辑错误。
  3. 相等处理:未明确元素相等时的出栈逻辑,影响单调性维护。
  4. 循环条件:遍历数组或处理栈操作时,循环方向判断错误,导致结果偏差。

7.2 调试方法:

  1. 打印栈状态:在关键操作处添加打印语句,跟踪栈内元素变化,定位逻辑错误。
  2. 小测试用例:使用简单、边界情况的测试用例,逐步验证算法正确性。
  3. 可视化辅助:通过画图、表格等方式,直观呈现栈的变化过程,辅助理解和调试。
  4. 暴力对比:将单调栈解法与暴力解法的结果进行对比,验证算法的准确性。

八、高频面试题扩展

  1. 接雨水(LeetCode 42):利用单调栈计算柱状图中能接住的雨水量。
  2. 子数组最小值之和(LeetCode 907):通过单调栈统计所有子数组中最小值的总和。
  3. 滑动窗口最大值(LeetCode 239):结合单调队列(单调栈的扩展应用)实现高效求解。
  4. 移掉 K 位数字(LeetCode 402):使用单调栈构造最小数字。
  5. 132 模式(LeetCode 456):通过单调栈判断数组中是否存在 132 模式的子序列。
// 接雨水解法示例
class Solution {public int trap(int[] height) {Deque<Integer> stack = new ArrayDeque<>();int total = 0;for (int i = 0; i < height.length; i++) {while (!stack.isEmpty() && height[i] > height[stack.peek()]) {int bottom = stack.pop();if (stack.isEmpty()) break;int left = stack.peek();int w = i - left - 1;int h = Math.min(height[left], height[i]) - height[bottom];total += w * h;}stack.push(i);}return total;}
}

九、性能优化策略

  1. 数组替代栈:使用int[]数组和指针模拟栈操作,减少对象创建开销,提升性能。
  2. 预处理高度:提前计算元素左侧 / 右侧第一个更大 / 更小元素的索引,避免重复计算。
  3. 剪枝优化:在遍历过程中,根据已知条件提前终止不必要的计算,减少运算量。
  4. 空间复用:复用结果数组或中间计算数组,减少内存分配与回收的开销。

十、学习路径建议

  1. 基础阶段:熟练掌握单调栈模板代码,通过经典题目理解其核心逻辑与应用场景。
  2. 进阶阶段:挑战二维矩阵、循环数组等扩展问题,提升问题转化与综合运用能力。
  3. 高手阶段:研究竞赛级优化方案,探索单调栈与其他数据结构、算法的结合应用。
  4. 面试准备:针对性练习高频面试题及其变种,总结解题思路与技巧,提升面试应变能力。

结语

单调栈作为算法领域中高效解决特定问题的工具,其核心思想与实现方式值得深入钻研。理解其如何通过维护单调性优化时间复杂度,是掌握这一算法的关键。在学习过程中,建议读者多画图辅助理解栈的变化过程,尝试将已有解法迁移到新问题中,并通过大量练习巩固知识。

推荐练习顺序: 496 → 739 → 503 → 42 → 84 → 85 → 907 → 239 → 402 → 456

本文所有 Java 代码均通过 LeetCode 平台测试,可直接用于面试准备与算法学习。欢迎在评论区分享你的学习心得、解题思路,或提出疑问,让我们共同探讨单调栈算法的魅力!

http://www.xdnf.cn/news/307009.html

相关文章:

  • [250504] Moonshot AI 发布 Kimi-Audio:开源通用音频大模型,驱动多模态 AI 新浪潮
  • Android数据库全栈开发实战:Room+SQLCipher+Hilt企业级应用构建
  • 【计算机网络】TCP/IP四层模型是什么?与OSI七层模型哪些区别?
  • 提示词的 嵌入空间优化
  • ECMAScript 6(ES6):JavaScript 现代化的革命性升级
  • 使用蚁群算法求解VRPTW问题
  • 信息系统项目管理工程师备考计算类真题讲解十三
  • 光纤失效模式及其影响
  • n8n 与智能体构建:开发自动化 AI 作业的基础平台
  • 单例模式的实现方法
  • Android SDK 国内镜像及配置方法(2025最新,包好使!)
  • MySQL同步ES的6种方案!
  • 74LS138译码器的编址技术
  • 存储系列知识
  • YOLO8之学习指南
  • 行业黑化.新平面
  • 系统学习算法:动态规划(斐波那契+路径问题)
  • 第2章——springboot核心机制
  • Spring Boot Validation实战详解:从入门到自定义规则
  • DXFViewer进行中2 -> 直线 解析+渲染 ✅已完成
  • 2025 RSAC|大语言模型应用风险与厂商攻防新策略
  • C#经典算法面试题
  • 【STM32 学习笔记】EXTI外部中断
  • 单片机-STM32部分:5、STM32CubeMX实现HAL点灯
  • Python之内省与反射应用
  • 多语言笔记系列:Polyglot Notebooks 中使用扩展库
  • Kotlin Android开发过渡指南
  • 【笔记】【B站课程 pytorch】梯度下降模型
  • 【2025年】基于电脑的jdk1.8通过idea创建springboot2.x版本(非常简洁快速)
  • 今日行情明日机会——20250506