代码随想录-单调栈

单调栈

单调栈基础概念

单调栈是一种特殊的栈数据结构,其核心特性是栈内元素保持单调递增或递减的顺序。它主要用于高效解决数组中元素与其最近更大或更小值的查找问题,时间复杂度可优化至 <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长度的木头,重复上面的步骤,这样就会计算出整个递增木头集合的最大面积,然后更新最大面积。

相关推荐
知识分享小能手8 分钟前
CSS3学习教程,从入门到精通,CSS3 浮动与清除浮动语法知识点及案例代码(14)
前端·css·后端·学习·html·css3·html5
Answer_ism6 小时前
【SpringMVC】SpringMVC拦截器,统一异常处理,文件上传与下载
java·开发语言·后端·spring·tomcat
盖世英雄酱581369 小时前
JDK24 它来了,抗量子加密
java·后端
Asthenia04129 小时前
无感刷新的秘密:Access Token 和 Refresh Token 的那些事儿
前端·后端
Asthenia041210 小时前
面试复盘:聊聊epoll的原理、以及其相较select和poll的优势
后端
luckyext10 小时前
SQLServer列转行操作及union all用法
运维·数据库·后端·sql·sqlserver·运维开发·mssql
Asthenia041210 小时前
ES:倒排索引的原理与写入分析
后端
每次的天空11 小时前
Android第五次面试总结(HR面)
android·面试·职场和发展
圈圈编码11 小时前
Spring常用注解汇总
java·后端·spring
stark张宇11 小时前
PHP多版本共存终极填坑指南:一台服务器部署多实例的最佳实践
后端·php