当前位置: 首页 > 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 平台测试,可直接用于面试准备与算法学习。欢迎在评论区分享你的学习心得、解题思路,或提出疑问,让我们共同探讨单调栈算法的魅力!

相关文章:

  • 探秘数据中台:五大核心平台的功能全景解析
  • 封装axios,实现取消请求
  • 个人码支付免签系统三网免挂支付宝微信QQ钱包即时到账收款二维码聚合支付源码
  • 可视化图解算法33:判断是不是平衡二叉树
  • 告别异步复杂性?JDK 21 虚拟线程让高并发编程重回简单
  • I2C总线驱动开发:MPU6050应用
  • 《Python星球日记》 第36天:线性代数基础
  • 网站防护如何无惧 DDoS 攻击?
  • 一周学会Pandas2 Python数据处理与分析-Pandas2数据类型转换操作
  • 邂逅蓝耘元生代:ComfyUI 工作流与服务器虚拟化的诗意交织
  • 【coze】手册小助手(提示词、知识库、交互、发布)
  • 黑马点评day04(分布式锁-setnx)
  • AI数据分析中的伪需求场景:现状、挑战与突破路径
  • RAG_Techniques:探索GitHub热门RAG技术开源项目
  • MySQL的内置函数与复杂查询
  • HarmonyOS 5.0 分布式数据协同与跨设备同步​​
  • 数据分析汇报七步法:用结构化思维驱动决策
  • 大数据应用开发和项目实战-电商双11美妆数据分析
  • 第八章,STP(生成树协议)
  • 【IP101】图像特征提取技术:从传统方法到深度学习的完整指南
  • 涉个人信息收集使用问题,15款App和16款SDK被通报
  • 无人机穿越大理崇圣寺千年古塔时“炸机”,当地:肇事者已找到,将被追责
  • “五一”假期国内出游3.14亿人次,同比增长6.4%
  • 新质观察|“模速空间”如何成为“模范空间”
  • 景点变回监狱,特朗普下令重新启用“恶魔岛”
  • “高校领域突出问题系统整治”已启动,聚焦招生、基建、师德等重点