代码随想录-单调栈

单调栈

单调栈基础概念

单调栈是一种特殊的栈数据结构,其核心特性是栈内元素保持单调递增或递减的顺序。它主要用于高效解决数组中元素与其最近更大或更小值的查找问题,时间复杂度可优化至 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N)

核心特性

  1. 单调性

    • 单调递增栈:从栈顶到栈底,元素严格递增。常用于寻找元素右侧第一个更小值。
    • 单调递减栈:从栈顶到栈底,元素严格递减。常用于寻找元素右侧第一个更大值。
  2. 工作原理

    • 遍历数组时,每个元素与栈顶元素比较:
      • 若当前元素破坏栈的单调性,则弹出栈顶元素,直到满足单调性为止。
      • 将当前元素压入栈中。
    • 通过这一过程,栈内始终维持单调性,从而快速定位目标值。

典型应用场景

  1. 寻找最近更大/更小元素

    • 对每个元素,找到其左侧或右侧第一个比它大或小的元素。例如:
      • 数组 [4, 2, 6, 1, 7] 中,使用单调递增栈可得到每个元素右侧最近的更小值 [2, 1, 1, -1, -1]
      • 数组 [2, 1, 4, 3, 5] 中,单调递减栈可找到右侧最近的更大值 [4, 4, 5, 5, -1][2]。
  2. 复杂问题优化

    • 柱状图最大矩形面积、滑动窗口极值、循环数组问题等。

实现示例(以单调递增栈为例)

java 复制代码
Stack<Integer> stack = new Stack<>();
for (int num : nums) {
    while (!stack.isEmpty() && stack.peek() > num) {
        stack.pop(); // 弹出破坏单调性的元素
    }
    stack.push(num);
}

总结

单调栈通过维护元素的单调顺序,将原本需 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N 2 ) O(N^2) </math>O(N2) 的遍历问题优化为线性复杂度。其关键在于通过弹栈操作动态调整栈结构,确保每次入栈后仍保持单调性。

T739-每日温度

见LeetCode第739题[每日温度]

题目描述

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

示例 1:

ini 复制代码
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

我的思路

本题要求解右侧最近较大元素,如果使用暴力求解,则时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N 2 ) O(N^2) </math>O(N2),可以考虑使用单调栈求解。

因为要求得右侧最近最大的元素的下标,因此,我们需要保证单调栈中的元素单调递减。对于破坏单调栈中的元素,需要将其弹出,记录待入栈元素的索引。

问题:如何获得栈顶元素的索引???

可以在栈中,将元素的下标和值都存储起来。

java 复制代码
/**
 * 每日温度
 * @param temperatures
 * @return
 */
public int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length;
    int[] records = new int[n];

    if (n <= 1) return records;

    Stack<int[]> stack = new Stack<>();
    for (int i = 0; i < n; i++) {

        while (!stack.empty() && stack.peek()[0] < temperatures[i]) {
            // 将栈顶元素弹出,并记录当前坐标,
            int[] top = stack.pop();
            records[top[1]] = i - top[1];
        }
        // 将当前元素入栈
        stack.push(new int[]{temperatures[i], i});
    }
    return records;
}

计算复杂度分析

  • 时间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N)
  • 空间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N)

优化思路

栈中不必要存储值,只需要存储坐标即可,可以使用坐标从数组中获取到值。

T496-下一个更大元素I

见LeetCode第496题[下一个更大元素I]

题目描述

nums1 中数字 x下一个更大元素 是指 xnums2 中对应位置 右侧第一个x ****大的元素。

给你两个 没有重复元素 的数组 nums1nums2 ,下标从 0 开始计数,其中nums1nums2 的子集。

对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j]下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1

返回一个长度为 nums1.length 的数组 **ans **作为答案,满足 **ans[i] **是如上所述的 下一个更大元素

示例

ini 复制代码
输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释: nums1 中每个值的下一个更大元素如下所述:
- 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
- 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。
- 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。

我的思路

首先处理集合nums2,使用单调栈,记录每一个元素的最近较大的元素值,然后将其存储到map中。因为是最近较大值,所以应该保证单调栈中的元素是单调递减的。

然后遍历子集nums1,对于元素num,从map中取得其最近较大值即可。

java 复制代码
private final HashMap<Integer, Integer> nextGreaterMap = new HashMap<>();
/**
 * 下一个更大的元素
 * @param nums1
 * @param nums2
 * @return
 */
public int[] nextGreaterElement(int[] nums1, int[] nums2) {

    // 通过单调栈获取每个元素的最近较大元素
    Stack<Integer> stack = new Stack<>();

    for (int i = 0; i < nums2.length; i++) {
        // 保证单调递减
        while (!stack.isEmpty() && nums2[stack.peek()] < nums2[i]) {
            nextGreaterMap.put(nums2[stack.peek()], nums2[i]);
            stack.pop();
        }
        stack.push(i);
    }

    for (int i = 0; i < nums1.length; i++) {
        nums1[i] = nextGreaterMap.getOrDefault(nums1[i], -1);
    }

    return nums1;
}

计算复杂度分析

  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( M + N ) O(M + N) </math>O(M+N),分别遍历了两个数组中的元素
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 2 × N ) O(2\times N) </math>O(2×N),一个哈希表,一个栈

T503-下一个更大的元素II

见LeetCode第503题[下一个更大的元素II]

题目描述

给定一个循环数组 numsnums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素

数字 x下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。

示例 1:

ini 复制代码
输入: nums = [1,2,1] 
输出: [2,-1,2] 
解释: 
第一个 1 的下一个更大的数是 2; 
数字 2 找不到下一个更大的数; 
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。

我的思路

循环数组的最近较大元素。

循环数组右边最近最大元素,可能出现在其左边。如果循环两遍数组的话,是不是就可以发现某个数的右边较大数在左边的情况呢?

答案貌似是肯定的,可以先试一下。

java 复制代码
private final HashMap<Integer, Integer> nextGreaterMap = new HashMap<>();
/**
 * 循环数组的下一个最大元素
 * @param nums
 * @return
 */
public int[] nextGreaterElements(int[] nums) {
    if (nums.length == 1) return new int[]{-1};
    int n = nums.length;

    Stack<Integer> stack = new Stack<>();

    for (int i = 0; i < 2 * n - 1; i++) {
        while (!stack.isEmpty() && nums[stack.peek()] < nums[i % n]) {
            // 记录当前值的最近最大值
            nextGreaterMap.putIfAbsent(stack.pop(), nums[i % n]);
        }
        stack.push(i % n);
    }

    Arrays.fill(nums, -1);
    for (Map.Entry<Integer, Integer> entry : nextGreaterMap.entrySet()) {
        nums[entry.getKey()] = entry.getValue();
    }
    return nums;
}

计算复杂度分析

  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 2 × N ) O(2\times N) </math>O(2×N)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 2 × N ) O(2\times N) </math>O(2×N)

优化思路

  • 使用数组来替代栈
  • 根本不需要使用Map,在迭代的过程中,直接将计算结果存储到结果数组中

T42-接雨水

见LeetCode第42题[接雨水]

题目描述

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例

css 复制代码
输入: height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
解释: 上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

我的思路

先从左边开始遍历,用变量curMax记录当前左边的最大值。如果当前的高度小于左边最大高度,则将当前的差值,记录在int[] waters中。如果当前的高度大于左边最大值,则更新最大值curMax。经过一轮循环之后,得到的数组waters右边柱子无限大的时候,理论能够盛到的最多的雨水。

同理,再从右边往左边遍历,相同的思路,再次更新waters,其中waters在更新的时候,为两次遍历的最小值 。此外,当当前柱子小于右边最大高度的时候,除了要更新最大高度curMax之外,还需要将当前的理论接水值waters[i]记为0。

最后,将waters[]中的所有值进行累加即可得到最终结果。

java 复制代码
/**
 * 接雨水
 * @param height
 * @return
 */
public int trap(int[] height) {
    if (height.length <= 2) return 0;
    int[] waters = new int[height.length];
    int leftMax = height[0];
    // 首先从左往右遍历
    for (int i = 1; i < height.length - 1; i++) {
        if (height[i] < leftMax) {
            waters[i] = leftMax - height[i];
        } else {
            leftMax = height[i];
        }
    }
    // 从右往左遍历
    int rightMax = height[height.length - 1];
    for (int i = height.length - 2; i > 0; i--) {
        if (height[i] < rightMax) {
            waters[i] = Math.min(waters[i], rightMax - height[i]);
        } else {
            waters[i] = 0;
            rightMax = height[i];
        }
    }

    return Arrays.stream(waters).sum();
}

计算复杂度分析

  • 时间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N),遍历了3次数组
  • 空间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N),中间数组用来存储理论可以接到的雨水

优化思路

其实不必要遍历两次数组,只需要一次遍历即可。

使用双指针的方式,左右两个指针交替更新,直到两个指针相遇。

使用变量分别记录左边的最大值leftMax,右边的最大值rightMax

先从左边开始遍历,如果当前的值height[l] < rightMax,则表示可以储水,并且水量为leftMax - height[l]。直到height[l] > rightMax,开始移动右边的指针right

java 复制代码
/**
 * 双指针解决接雨水
 * @param height
 * @return
 */
public int trapI(int[] height) {
    if (height.length <= 2) return 0;

    int n = height.length;
    int l = 0;
    int r = n - 1;
    int leftMax = 0;
    int rightMax = 0;

    int waters = 0;

    while (l < r) {
        // 更新左右的最高柱子
        leftMax = Math.max(leftMax, height[l]);
        rightMax = Math.max(rightMax, height[r]);
        // 如果当前的柱子小于右边最大柱子,则表示当前的柱子可以储水
        if (height[l] < rightMax) {
            waters += leftMax - height[l++];
        } else {
            // 移动右边的指针,右边柱子小于左边最高柱子
            waters += rightMax - height[r--];
        }
    }
    return waters;
}

计算复杂度分析

  • 时间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N)
  • 空间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)

T84-柱状图的最大矩形

见LeetCode第84题[柱状图的最大矩形]

题目描述

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

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

示例

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

我的思路

这个题目像是木桶效应,如何算出来木桶的面积?

首先思考一个问题,从左到右的每一根木头,我们想要木块是怎么样的规律?单调递增还是单调递减?

答案显而易见,单调递增。因为我们是从左往右遍历,如果不满足递增,还可以拐回来,将之前的木头锯一下,总不可能是为了单调递减补一下之前的木头吧。

有了目标,这样我们就可以使用我们本章的主题【单调栈】来确保我们的木头都是单调递增的了。从左到右依次遍历我们的木头集合,

如果当前待入栈的木头大于栈顶木头,应该怎么处理?先把栈顶木头拉出来,计算一下他的长度,然后跟新一下最大的面积,然后将其用锯子锯为次高值。注意,此时可能有多根木头被锯为次高值。

如果发现次高值还是不行,还是大于当前的木头,那么再次重复此步骤,并更新最大值,直到满足栈顶木头小于等于当前的木头,然后当前的木头进栈。

遍历完最后一根木头之后,我们需要往里面添加最后一根0长度的木头,重复上面的步骤,这样就会计算出整个递增木头集合的最大面积,然后更新最大面积。

相关推荐
㳺三才人子2 小时前
初探 Flask
后端·python·flask·html
星栈独行2 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Java爱好狂.2 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易2 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
装不满的克莱因瓶3 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
Raink老师3 小时前
【AI面试临阵磨枪-62】设计基于 RAG 的内部知识库问答平台(多租户、权限、文件上传、实时更新)
人工智能·面试·职场和发展
ltl3 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
excel4 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
卷毛的技术笔记5 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
Raink老师5 小时前
【AI面试临阵磨枪-68】设计一个端侧(手机 / 浏览器)轻量化 AI Agent 系统
人工智能·面试·智能手机