单调栈【JAVA刷题 自存】


每日温度

请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。

例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。

提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。

思路

单调栈的用处:通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为O(n)。

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

更直白来说,就是用一个栈来记录我们遍历过的元素,因为我们遍历数组的时候,我们不知道之前都遍历了哪些元素,以至于遍历一个元素找不到是不是之前遍历过一个更小的,所以我们需要用一个容器(这里用单调栈)来记录我们遍历过的元素。

在使用单调栈的时候首先要明确如下几点:

1.单调栈里存放的元素是什么?
单调栈里只需要存放元素的下标i就可以了,如果需要使用对应的元素,直接T[i]就可以获取。

2.单调栈里元素是递增呢? 还是递减呢?
注意以下讲解中,顺序的描述为 从栈头到栈底的顺序,因为单纯的说从左到右或者从前到后,不说栈头朝哪个方向的话,大家一定比较懵。

这里我们要使用递增循序(再强调一下是指从栈头到栈底的顺序),因为只有递增的时候,栈里要加入一个元素i的时候,才知道栈顶元素在数组中右面第一个比栈顶元素大的元素的下标是i。

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

使用单调栈主要有三个判断条件。

  • 当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况
  • 当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况
  • 当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况
class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        //首先声明一个栈
        //然后要明确从栈顶到栈底应该是单调递增
        //每遍历一个元素(a) 就和栈顶(st.top)比较
        /*
        情况一:
        如果遍历的元素(a)比栈顶(st.top)大 那么就把栈顶(st.top())弹出
        并且把遍历的元素a的下标 与栈顶元素(st.top)相减 
        代表st.top这个元素 右边第一个比他大的元素就是(a) 相减后的距离即为所求
        并且这里要一直弹出 直到栈里面没有比元素小的元素为止
        然后把元素a入栈
        */

        /*
        情况二:
        如果如果遍历的元素(a)与栈顶(st.top)相等
        那么元素a入栈,因为题目问的是第一个大于的元素,不是第一个大于等于的元素
        */

        /*
        情况三:
        如果遍历的元素(a)比栈顶(st.top)小 
        那么a入栈
        */

        // 1. 获取数组长度
        int len = temperatures.length;

        // 2. 声明结果数组
        int[] res = new int[len];

        // 3. 声明栈
        Stack<Integer> stack = new Stack<>();

        // 4. 遍历温度数组
        for (int i = 0; i < len; i++) {
            // 比较当前温度和栈顶温度的大小
            while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
                // 栈顶温度的下标和当前温度的下标之差即为结果
                int index = stack.pop();
                res[index] = i - index;
            }
            // 将当前元素下标入栈
            stack.push(i);
        }

        return res;
    }
    
}

下一个更大元素 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 。

思路

没有重复元素,我们就可以用map来做映射了。根据数值快速找到下标,还可以判断nums2[i]是否在nums1中出现过。

本题的单调栈还是栈顶到栈底从小到大。

class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        Stack<Integer> st = new Stack<>();
        st.push(0);


        int len = nums1.length;

        int[] res = new int[len];
        Arrays.fill(res,-1);

        HashMap<Integer,Integer> map = new HashMap<>();
        
        for(int i=0;i<len;i++){
            //注意map的方法是put 不是push
            map.put(nums1[i],i);
        }
        for(int i=1;i<nums2.length;i++){
            //java的栈 栈顶是peek
            if(nums2[i]<=nums2[st.peek()]){
                st.push(i);
            }else{
                while(!st.isEmpty()&&nums2[i]>nums2[st.peek()]){
                    //注意这里面讨论的都是st.top()
                     if(map.containsKey(nums2[st.peek()])){
    res[map.get(nums2[st.peek()])] = nums2[i];
}

                    //这里注意 不管nums1里面有没有出现都要弹出来
                    st.pop();
                }
               st.push(i);//注意弹出来以后要放进去
            }
        }
        return res;
    }
}

下一个更大元素II

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

示例 1:

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

思路

解法一:

将两个nums数组拼接在一起,使用单调栈计算出每一个元素的下一个最大值,最后再把结果集即result数组resize到原数组大小就可以了。

解法二:

这种写法确实比较直观,但做了很多无用操作,例如修改了nums数组,而且最后还要把result数组resize回去。

resize倒是不费时间,是O(1)的操作,但扩充nums数组相当于多了一个O(n)的操作。

其实也可以不扩充nums,而是在遍历的过程中模拟走了两边nums。

class Solution {
    public int[] nextGreaterElements(int[] nums) {
         int len = nums.length;
         if(len==1){
            return new int[]{-1};
         }
         int[] res = new int[len];
         Arrays.fill(res,-1);
         Stack<Integer> stack = new Stack<>();
         stack.push(0);
        for (int i = 1; i < len*2; i++) {
            while (!stack.isEmpty() && nums[i%len] > nums[stack.peek()]) {
                res[stack.peek()]=nums[i%len];
                stack.pop();
            }
            stack.push(i%len);
        }
        return res;
    }
}

接雨水

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

示例 1:
在这里插入图片描述
输入: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 个单位的雨水(蓝色部分表示雨水)。

思路

一、暴力解法

  1. 选择按行计算还是按列计算 -->选用列更好理解
  2. 求每一列雨水高度就可以了
  3. 每一列雨水的高度=该列 左侧最高的柱子和右侧最高的柱子中 较矮的那个柱子的高度 - 当前列的柱子高度。
class Solution {
    public int trap(int[] height) {
        int len = height.length;
        int sum = 0;

        // 注意第一根柱子和最后一根柱子肯定不存水
        for (int i = 1; i < len - 1; i++) {
            int lH = 0;  // 初始化左侧最高高度为 0
            int rH = 0;  // 初始化右侧最高高度为 0

            // 找左侧最高的柱子
            for (int l = i - 1; l >= 0; l--) {
                lH = Math.max(lH, height[l]);
            }

            // 找右侧最高的柱子
            for (int r = i + 1; r < len; r++) {
                rH = Math.max(rH, height[r]);
            }

            // 当前柱子能存的水量 = min(左侧最高, 右侧最高) - 当前高度
            if (Math.min(lH, rH) > height[i]) {
                sum += Math.min(lH, rH) - height[i];
            }
        }
        return sum;
    }
}

二、双指针优化

当前列雨水面积:min(左边柱子的最高高度,记录右边柱子的最高高度) - 当前柱子高度。

为了得到两边的最高高度,使用了双指针来遍历,每到一个柱子都向两边遍历一遍,这其实是有重复计算的。我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight),这样就避免了重复计算。

当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。

即从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i - 1]);

从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]);

class Solution {
    public int trap(int[] height) {
        if (height == null || height.length <= 2) {
            // 如果高度数组为空或长度小于等于2,不可能存水
            return 0;
        }

        int maxLeft = height[0];  // 初始化左边的最高柱子
        int maxRight = height[height.length - 1];  // 初始化右边的最高柱子
        int l = 1;  // 左指针,从左边第二根柱子开始
        int r = height.length - 2;  // 右指针,从右边倒数第二根柱子开始
        int res = 0;  // 存储结果,即总的接水量

        while (l <= r) {
            // 每轮更新左右两边的最大高度
            maxLeft = Math.max(maxLeft, height[l]);
            maxRight = Math.max(maxRight, height[r]);

            if (maxLeft < maxRight) {
                // 如果左边的最高柱子比右边的小,则左边的水量已确定
                res += maxLeft - height[l];  // 计算当前柱子能存的水量
                l++;  // 左指针向右移动
            } else {
                // 如果右边的最高柱子比左边的小或相等,则右边的水量已确定
                res += maxRight - height[r];  // 计算当前柱子能存的水量
                r--;  // 右指针向左移动
            }
        }
        return res;  // 返回总的接水量
    }
}

三、单调栈

  1. 首先单调栈是按照行方向来计算雨水
  2. 单调栈内元素是递增还是递减呢?
  • 从栈顶到栈底的顺序应该是从小到大的顺序。
  • 因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。
    在这里插入图片描述
  1. 遇到相同高度的柱子怎么办
  • 遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。
  • 因为我们要求宽度的时候 如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度。
  1. 栈里要保存什么数值
  • 使用单调栈,也是通过 长 * 宽 来计算雨水面积的。
  • 长就是通过柱子的高度来计算,宽是通过柱子之间的下标来计算,
  • 栈里就存放下标就行,想要知道对应的高度,通过height[stack.top()] 就知道弹出的下标对应的高度了。
  1. 单调栈处理逻辑
  • 基本逻辑与之前相同
  • 【重点】如果当前遍历的元素(柱子)高度大于栈顶元素的高度,此时就出现凹槽了。
  • 取栈顶元素,将栈顶元素弹出,这个就是凹槽的底部,也就是中间位置,下标记为mid,对应的高度为height[mid](就是图中的高度1)。
  • 此时的栈顶元素st.top(),就是凹槽的左边位置,下标为st.top(),对应的高度为height[st.top()](就是图中的高度2)。
  • 当前遍历的元素i,就是凹槽右边的位置,下标为i,对应的高度为height[i](就是图中的高度3)。
  • 那么雨水高度是 min(凹槽左边高度, 凹槽右边高度) - 凹槽底部高度,代码为:int h = min(height[st.top()], height[i]) - height[mid];

雨水的宽度是 凹槽右边的下标 - 凹槽左边的下标 - 1(因为只求中间宽度),代码为:int w = i - st.top() - 1 ;

当前凹槽雨水的体积就是:h * w。

class Solution {
    public int trap(int[] height){
        int size = height.length;

        if (size <= 2) return 0;

        // in the stack, we push the index of array
        // using height[] to access the real height
        Stack<Integer> stack = new Stack<Integer>();
        stack.push(0);

        int sum = 0;
        for (int index = 1; index < size; index++){
            int stackTop = stack.peek();
            if (height[index] < height[stackTop]){
                stack.push(index);
            }else if (height[index] == height[stackTop]){
                // 因为相等的相邻墙,左边一个是不可能存放雨水的,所以pop左边的index, push当前的index
                stack.pop();
                stack.push(index);
            }else{
                //pop up all lower value
                int heightAtIdx = height[index];
                while (!stack.isEmpty() && (heightAtIdx > height[stackTop])){
                    int mid = stack.pop();

                    if (!stack.isEmpty()){
                        int left = stack.peek();

                        int h = Math.min(height[left], height[index]) - height[mid];
                        int w = index - left - 1;
                        int hold = h * w;
                        if (hold > 0) sum += hold;
                        stackTop = stack.peek();
                    }
                }
                stack.push(index);
            }
        }

        return sum;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值