栈7:单调栈的原理和应用

​在刷LeetCode之前 ,我看到过单调栈,感觉就是在栈的基础上加了个要求而已,没怎么当回事。但是在刷LeetCode题目的时候,才知道有些题用单调栈非常好使。本着学会一个,刷掉一片的原则,我们用几篇来研究一下这个结构,以及相关的题目。

1.单调栈的概念

单调栈是前一节介绍的最大最小栈的内涵是相通的,单调栈就是栈里面存放的数据都是有序的,所以可以分为单调递增栈和单调递减栈两种。

单调递增栈就是从栈底到栈顶是从大到小

单调递减栈就是从栈底到栈顶是从小到大

为什么要使用单调栈?

why搞出一个单调栈的东西?哲学认为凡是存在即合理,而算法里之所以搞出这么个东西,一定是在解决某些问题的时候特别好用,然后就给起了个名字(如果这样那是不是会有个单调队列呢?有的!优先级队列不就算吗?)。

言归正传,就拿LeetCode503. 下一个更大元素 II 这道题来讲,如果利用常规解法,对于每个数而言,我们需要遍历其右边的数,直到找到比自身大的数,这是一个 O(n^2) 的做法。之所以是 O(n^2),是因为每次找下一个最大值,我们是通过遍历来实现的。如果面试的时候遇到这个问题了你采用常规解法可能你就gg了。而使用单调栈的话你就可以将复杂度降到O(n),这就是你为啥要使用单调栈的原因了。

2.什么时候使用单调栈?

问题又来了啥时候使用呢?一般来讲,【找左右两边比你大或者比你小】的问题,都可以使用单调栈来解决。所以遇到这种问题一定要记得考虑一下单调栈能不能解决问题。

单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录左(右)边第一个比当前元素大(小)的元素,优点是只需要遍历一次。

单调栈里存放的元素是什么?

单调栈里只需要存放元素的下标i就可以了,如果需要使用对应的元素,直接用 nums[i] 就可以获取

 下面我们看几个例子:

 3.LeetCode496:下一个更大元素I

先看题目:

给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。

请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。

nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。

例子1:

输入: nums1 = [4,1,2], nums2 = [1,3,4,2].输出: [-1,3,-1]解释:    对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。    对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。    对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。

例子2:

输入: nums1 = [2,4], nums2 = [1,2,3,4].输出: [3,-1]解释:    对于 num1 中的数字 2 ,第二个数组中的下一个较大数字是 3 。    对于 num1 中的数字 4 ,第二个数组中没有下一个更大的数字,因此输出 -1 。

首先,该题使用暴破的时间复杂度(n*m)偏高,所以为了降低时间复杂度,我们采用另外一种方式:单调栈。这个题目有点长,其实意思很简单,就是先对第二个按照从小到大排一下顺序,为了便于查找时操作,我们用栈来存储,同时还使用了Hash来保存对应关系。

你可能会有疑问,该方法好在哪里呢?其实该方法采用先行将 nums2中的数字,对应的下一个更大的数字已经找出来,然后放到哈希表中,以供后面 nums1直接使用即可,我们这样做,可以将时间复杂度降到 n + mn+m。

具体流程如下:

  1. 创建一个临时栈,一个哈希表,然后遍历 nums2。

  2. 若当前栈无数据,则当前数字入栈备用。

  3. 若当前栈有数据,则用当前数字与栈顶比较:

        当前数字 > 栈顶,代表栈顶对应下一个更大的数字就是当前数字,则将该组数字对应关系,记录到哈希表。

      当前数字 < 栈顶,当前数字压入栈,供后续数字判断使用。

      4.这样,我们就可以看到哈希表中存在部分 nums2 数字的对应关系了,而栈中留下的数字,代表无下一个更大的数字,我们全部赋值为 −1 ,然后存入哈希表即可。

      5.遍历 nums1,直接询问哈希表拿对应关系即可。

public class Solution {    public int[] nextGreaterElement(int[] nums1, int[] nums2) {        int len1 = nums1.length;        int len2 = nums2.length;        Deque<Integer> stack = new ArrayDeque<>();        Map<Integer, Integer> map = new HashMap<>();        for (int i = 0; i < len2; i++) {            while (!stack.isEmpty() && stack.peekLast() < nums2[i]) {                map.put(stack.removeLast(), nums2[i]);            }            stack.addLast(nums2[i]);        }        int[] res = new int[len1];        for (int i = 0; i < len1; i++) {            res[i] = map.getOrDefault(nums1[i], -1);        }        return res;    }}

4.举例 LeetCode 503 下一个更大元素II

这个题也是对上面题目的条件做了改动,先看题目要求:

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

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

我们可以使用单调栈解决本题。单调栈中保存的是下标,从栈底到栈顶的下标在数组 nums 中对应的值是单调不升的。

每次我们移动到数组中的一个新的位置 i,我们就将当前单调栈中所有对应值小于nums[i] 的下标弹出单调栈,这些值的下一个更大元素即为nums[i](证明很简单:如果有更靠前的更大元素,那么这些位置将被提前弹出栈)。随后我们将位置 i入栈。

但是注意到只遍历一次序列是不够的,例如序列 [2,3,1],最后单调栈中将剩余 [3,1],其中元素 [1] 的下一个更大元素还是不知道的。我们要找每一个元素的下一个更大的值,因此我们需要对原数组遍历两次,对遍历下标进行取余转换。

建立「单调递减栈」,并对原数组遍历一次:

  • 如果栈为空,则把当前元素放入栈内;

  • 如果栈不为空,则需要判断当前元素和栈顶元素的大小:

          1.如果当前元素比栈顶元素大:说明当前元素是前面一些元素的「下一个

           更大元素」,则逐个弹出栈顶元素,直到当前元素比栈顶元素小为止。

          2.如果当前元素比栈顶元素小:说明当前元素的「下一个更大元素」与 

          栈顶元素相同,则把当前元素入栈。

上代码:

 public int[] nextGreaterElements(int[] nums) {        int n = nums.length;        int[] ret = new int[n];        Arrays.fill(ret, -1);        Deque<Integer> stack = new LinkedList<Integer>();        for (int i = 0; i < n * 2 ; i++) {            while (!stack.isEmpty() && nums[stack.peek()] < nums[i % n]) {                ret[stack.pop()] = nums[i % n];            }            stack.push(i % n);        }        return ret;    }

可能刚开始有点看不懂没关系可以尝试一下测试版:

public int[] nextGreaterElements(int[] nums) {        int n = nums.length;        int[] ret = new int[n];        Arrays.fill(ret, -1);        Deque<Integer> stack = new LinkedList<Integer>();        for (int i = 0; i < n * 2 - 1; i++) {            System.out.println("第"+i+"轮循环");            while (!stack.isEmpty() && nums[stack.peek()] < nums[i % n]) {                System.out.println("数字"+nums[stack.peek()]+"小于"+"数字"+nums[i % n]);                System.out.println("让ret"+stack.peek()+"的值为"+nums[i % n]+",弹出"+stack.peek());                ret[stack.pop()] = nums[i % n];            }            stack.push(i % n);            System.out.println("压入元素"+stack.peek());            System.out.println(" ");        }        return ret;    }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵横千里,捭阖四方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值