LeetCode:单调栈万能模板和配套题目(Java版)

1,003 阅读5分钟

参考链接:《单调栈解题模板秒杀三道算法题》--labuladong

最近买了《剑指Offer专项突破版》,做到栈这一章,栈最经典的运用就是单调栈。书里列出几道单调栈的题目:

  • 小行星碰撞
  • 每日温度
  • 直方图最大矩形面积
  • 矩阵中的最大矩形

做完前面两道刚好看到 labuladong 写的单调栈模板,感觉相见恨晚。只有你做了几道相近题之后,再回过头看人家总结的模板才比较有感觉。如果一上来就让你看模板很明显拿不符合认知的先后流程,难以理解透彻,纯属个人体会。

单调栈模板可以用一句话总结:Next Greater Number

既然是下一个更大的元素,那从数组尾部逆序遍历,这样再结合入栈出栈,会带来很大的方便,代码统一,更接近单调栈模板方法,以不变应万变。

一、单调栈模板方法:每日温度

下面从每日温度这道题切入,是一个最简单的单调栈模板题目(弄懂之前不敢这么说,但理解后确实是很符合这个单调栈模板方法,很清晰简洁):

image.png

class Solution {   
    //《单调栈模板方法》:逆序遍历数组,然后判断栈是否为空来作为当前元素是否有下一个更大元素的依据。
    public int[] dailyTemperatures(int[] temperatures) {
        int n = temperatures.length;
        int[] res = new int[n];
        Stack<Integer> stack = new Stack<>();
        for (int i = n - 1; i >= 0; i--) {
            while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[i]) {
                stack.pop();
            }
            //只有这一步会变,其他步骤是相对固定的。但也是以栈是否为空来作为判断是否满足题目要求的条件
            res[i] = stack.isEmpty() ? 0 : stack.peek() - i;
            stack.push(i);
        }
        return res;
    }
}

以上就是单调栈的模板方法,模板流程拆解:

  1. 从后到前逆序遍历目标数组
  2. 利用单调栈的单调递减性来决定栈顶元素是否需要出栈。
  3. 重点:以栈是否为空来作为是否满足题目条件的依据进行处理。不为空说明符合条件,为空说明栈里之前没有一个元素满足条件。比如,由于是逆序,说明当前元素之后没有任何元素大于它。
  4. 入栈。
  • 何时出栈:如果新元素大于栈顶元素,那么此时如果新元素入栈,就不符合单调递减性。因此此时需要将栈顶元素循环弹出,直到符合单调递减性为止。
  • 何时入栈:满足单调递减性才能入栈。

这里满足单调递减性有两种情况,栈被清空,或栈顶元素大于新元素。此时新元素入栈,满足单调递减性。

二、单调栈模板方法: 496. 下一个更大元素 I

496. 下一个更大元素 I 跟每日温度是非常相似的题目,接下来进入举一反二环节。本题比每日温度这道中等题难一些,但标签反而是easy。

区别只在于借助了额外的数据结构 HashMap,它可以存错遍历过程中所有元素的下一个更大值。最后在遍历子数组从 HashMap 里以 O(1) 时间复杂度获取目标值。

image.png

class Solution {
    //《单调栈目标方法》
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        int n = nums2.length;
        Stack<Integer> stack = new Stack<>();
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = n - 1; i >= 0; i--) {
            while(!stack.isEmpty() && nums2[i] > stack.peek()) {
                stack.pop();
            }
            int nextGreater = stack.isEmpty() ? -1 : stack.peek();//因为是从数组尾部遍历入栈的,因此栈顶肯定是位于当前元素之后
            map.put(nums2[i], nextGreater);
            stack.push(nums2[i]);
        }
        int[] res = new int[nums1.length];
        for (int i = 0; i < nums1.length; i++) {
            res[i] = map.get(nums1[i]);//借助 Map 很方便
        }
        return res;
    }
}

本题有更简单的版本,在 labuladong 的文章里给出里以下简单版本:

输入一个数组nums = [2,1,2,4,3],你返回数组[4,2,4,-1,-1]

也就是说,求所有元素的 Next Greater Element,不涉及子数组。因此就不需要再借助 HashMap 来快速获取子数组对应的值,因为逆序一遍下来所有元素都遍历完,且得到了每个元素下一个更大的元素值。

image.png

class Solution {
    //《单调栈模板方法》
    public int[] nextGreaterElement(int[] nums) {
        int[] res = new int[nums.length];
        Stack<Integer> stack = new Stack<>();
        for(int i = nums.length; i >= 0; i--) {
            //不满足单调递减,弹出直到满足了下面才入栈
            while (!stack.isEmpty() && nums[i] > stack.peek()) stack.pop();
            //所有元素都会得到结果
            res[i] = stack.isEmpty() ? -1 : stack.peek();
            //所有元素最终都会入栈
            stack.push(nums[i]);
        }
        return res;
    }
}

下面是labuladong对时间复杂度的分析,引用如下:

“这个算法的时间复杂度不是那么直观,如果你看到 for 循环嵌套 while 循环,可能认为这个算法的复杂度也是O(n^2),但是实际上这个算法的复杂度只有O(n)

分析它的时间复杂度,要从整体来看:总共有n个元素,每个元素都被push入栈了一次,而最多会被pop一次,没有任何冗余操作。所以总的计算规模是和元素规模n成正比的,也就是O(n)的复杂度。”

Hard 42. 接雨水

接雨水这道题是 hard 题目,刚开始做不知道套路就别死磕了,反正我做不出来。一开始不知道单调栈的套路,直接看了官方题解的双指针往里夹逼结合贪心算法来获取雨水面积,题解就不附上了。这里要讲的是单调栈解法:

image.png


//单调递减栈法:按行计算
public int trap(int[] height) {
    Deque<Integer> stack = new LinkedList<>();
    int sum = 0;
    for (int i = 0; i < height.length; i++) {//单调递减栈,从左往右遍历,按行计算雨水
        while (!stack.isEmpty() && height[i] > height[stack.peek()]) { //出现凹槽
            int low = stack.pop();
            if (stack.isEmpty()) break;
            //左边比 top 大,右边也比 top 大,形成了下凹,可以积水。
            int L = stack.peek();
            int shorter = Math.min(height[L], height[i]);//左边的边界可能比右边大,水量取决于短板
            int distance = i - L - 1;//手动推算即可
            //由于是单调递减栈,栈顶是最小的,越往栈底越大(下标对应的高度):cur = stack.pop() 出栈顶之后,新栈顶 left > cur
            sum += distance * (shorter - height[low]);//凹槽两边的短的那个板减去凹槽
        }
        stack.push(i);//存入的是下标,你可以根据下标在 O(1) 时间从数组获取到对应的元素值
    }
    return sum;
}

套路是不是似曾相识。这里按行计算很简洁,不过一般是想不出来这种解法的,直接看官方题解的动画即可。

但是共同点就是利用单调栈的性质,这里是单调递减性:

遍历过程中,后面元素比栈顶小才能入栈(符合单调递减)。当遇到比栈顶大的元素,栈顶出栈,这点是非常固定的,都是通过一个while loop 来循环出栈,直到满足单调递减性为止才将新元素入栈。