算法数据结构--单调栈

介绍

单调栈是一种特殊的栈数据结构,其特点在于栈内的元素保持单调性。单调栈通常分为单调递增栈和单调递减栈两种类型。

单调递增栈

​ 单调递增栈中的元素从栈底到栈顶依次递增。当我们向单调递增栈中压入一个元素时,如果该元素比栈顶元素大,就直接入栈;如果该元素比栈顶元素小,则不断将栈顶元素出栈,直到栈为空或者栈顶元素小于等于当前元素,然后再将当前元素入栈。单调递增栈主要用于解决一些需要找到右边第一个较大元素的问题。

单调递减栈

​ 单调递减栈中的元素从栈底到栈顶依次递减。当我们向单调递减栈中压入一个元素时,如果该元素比栈顶元素小,就直接入栈;如果该元素比栈顶元素大,则不断将栈顶元素出栈,直到栈为空或者栈顶元素大于等于当前元素,然后再将当前元素入栈。单调递减栈主要用于解决一些需要找到右边第一个较小元素的问题。

图示

在这里插入图片描述

应用场景

单调栈在解决一些数组或序列相关问题时非常有效。

例如:

  • 寻找数组中下一个较大或较小元素;
  • 计算滑动窗口的最大值或最小值;
  • 解决一些与序列相关的动态规划问题;
  • 解决一些需要找到数组中局部极值或单调性的问题等。

步骤

单调栈的步骤一般分为:

  1. 定义一个栈

    Deque<Integer> deque=new ArrayDeque<Integer>();
    
  2. 确定栈里存储的数据是什么

    一般情况下单调栈里面存放的都是目标数组的下标,这样做的好处就是,我们不仅知道遍历过的元素下标,而且也可以通过下标找到对应数组里面的元素。

  3. 确定使用单调递增栈还是单调递减栈

    如果求一个元素右边第一个更大元素时,单调栈就是递增的,如果求一个元素右边第一个更小元素,单调栈就是递减的。

  4. 遍历目标数组,比较当前遍历的元素与栈顶元素,并进行处理
    这里在遍历目标数组的时候,需要比较当前遍历元素和栈顶元素的大小。
    这个时候就有如下三种情况:

    • 当前遍历的元素小于栈顶元素的情况
    • 当前遍历的元素等于栈顶元素的情况
    • 当前遍历的元素大于栈顶元素的情况

    一般情况下单调递增栈在当前遍历的元素大于栈顶元素的情况下进行主要的逻辑处理,其他情况下直接把当前元素做入栈操作;而单调递减栈在当前遍历的元素小于栈顶元素的情况下进行主要的逻辑处理,其他情况下直接把当前元素做入栈操作。

    注意:在对栈进行操作的时候,要尽量减少对栈的操作次数。比如,如果在获取栈顶元素后,还要进行弹出操作且再获取栈顶元素后不需要在将该元素保留栈顶。则此时,可以直接使用pop()方法,代替使用peek()方法后再使用pop()方法。

  5. 返回结果

模板

import java.util.*;

public class MonotonicStackSolver {
    
    // 单调递增栈
    public void solveIncreasing(int[] nums) {
        Deque<Integer> stack = new ArrayDeque<>();
        // 遍历数组
        for (int i = 0; i < nums.length; i++) {
            // 维护单调性
            while (!stack.isEmpty() && nums[i] < stack.peek()) {
                stack.pop();
                // 在这里可以执行相关操作
            }
            // 入栈
            stack.push(nums[i]);
        }
    }

    // 单调递减栈
    public void solveDecreasing(int[] nums) {
        Deque<Integer> stack = new ArrayDeque<>();
        // 遍历数组
        for (int i = 0; i < nums.length; i++) {
            // 维护单调性
            while (!stack.isEmpty() && nums[i] > stack.peek()) {
                stack.pop();
                // 在这里可以执行相关操作
            }
            // 入栈
            stack.push(nums[i]);
        }
    }

    public static void main(String[] args) {
        MonotonicStackSolver solver = new MonotonicStackSolver();
        
        // 示例用法
        int[] nums = {2, 5, 9, 3, 1, 12, 6, 8};
        solver.solveIncreasing(nums); // 使用单调递增栈解决问题
        // 或者
        solver.solveDecreasing(nums); // 使用单调递减栈解决问题
    }
}

Deque用法

我们在用Java写单调栈题目的时候一般需要用到以下这几个方法:

  • deque.isEmpty():如果deque不包含任何元素,则返回true,否则返回false。因为要栈顶元素在满足要求的时候要弹出,所以需要进行空栈判断。有些场景,可能栈一定不会空的时候,就不需要该方法进行空栈判断。
  • deque.push(e):将元素e入栈。
  • deque.pop():将栈顶元素弹出,并返回当前弹出的栈顶元素。
  • deque.peek():获取栈顶元素,不弹出。

例题

下面我们用两个简单的例题来加深我们对单调栈的理解:

739. 每日温度

在这里插入图片描述

本题意思就是让我们去找每一个元素下一个比他更大的元素出现在后面的哪里,如果有则记录下标之差,如果没有则设置为0,最后返回答案数组。

我们看下代码:

class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int[] ans = new int[temperatures.length];//定义答案数组
        Deque<Integer> stack = new ArrayDeque<Integer>();//定义一个栈
        for (int i = 0; i < temperatures.length; i++) {
            while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {//如果栈不为空且遍历的元素大于栈顶元素,那么就要出栈,并且此时也证明栈顶元素找到了下一个比它更大的元素的位置,记录下标之差就可以了
                int m = stack.pop();//弹出栈顶元素(下标)
                ans[m] = i - m;//用此时遍历的元素下标减去栈顶元素下标就是栈顶元素下标下一个比他温度更高的天数
            }
            stack.push(i);//其他情况(栈为空或者遍历元素小于等于栈顶元素)都入栈
        }
        return ans;//返回答案
    }
}

我们往栈内存储的元素是下标而不是元素数值,这样我们就可以很容易地求出下标之差。

用Deque比自己创建一个数组来表示栈更方便一点

496. 下一个更大元素 I

在这里插入图片描述

本题就是让我们去找在nums1中出现过的数字在nums2中去找比它大的第一个数字是多少,答案要存储在nums1中的对应下标位置。

本题我们很容易就可以联想到要使用哈希表去存储nums1中出现过的数字,当循环nums2数组的时候就可以直接判断存在不存在了。

下面我们直接来看代码:

class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        int[] hash = new int[10001];
        for (int i = 0; i < nums1.length; i++) {
            hash[nums1[i]] = i + 1;//这里可以将哈希表对应下标位置的数值直接设置成i+1,这样在之后记录答案的时候直接在答案数组hash[]-1的位置记录就可以了
        }
        Deque<Integer> stack = new ArrayDeque<Integer>();
        int[] ans = new int[nums1.length];
        for (int i = 0; i < nums1.length; i++)
            ans[i] = -1;
        for (int i = 0; i < nums2.length; i++) {
            while(!stack.isEmpty()&&nums2[i]>nums2[stack.peek()]){
                int m = stack.pop();
                ans[hash[nums2[m]]-1] = nums2[i];
            }
            if(hash[nums2[i]]!=0)//遍历的数字在nums1中出现过才能入栈
                stack.push(i);
        }
        while(!stack.isEmpty()){
            int m = stack.pop();
            ans[hash[nums2[m]]-1] = -1;//栈内剩余的部分就是后面没有比他大的数字,直接设置成-1就可以了
        }
        return ans;
    }
}

总结

总的来说,单调栈是一种高效、简洁且灵活的数据结构。

单调栈的空间复杂度通常很低,因为它只需要存储输入序列中的一部分元素,而不需要额外的空间。

通常情况下,单调栈的时间复杂度为 O(n),其中 n 为输入序列的长度。这是因为在大多数情况下,每个元素最多进栈一次、出栈一次,所以整个过程的时间复杂度是线性的。

所以单调栈在解决数组或序列相关问题时具有很好的效果。

已经到底啦!!

  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值