算法学习(4):LeetCode刷题之单调栈

前言

栈是一种很常用的数据结构,最大的特点就是只能在一端进行操作。Java中的集合提供了一个接口Deque来表示栈结构,如下语句:

Deque<Integer> stack = new ArrayDeque<>();

至于为啥不用Stack类,请看下面这篇文章:
Java 程序员,别用 Stack?!

那么,单调栈又是什么呢?很简单,就是普通的栈加一个限定条件:栈内元素单调递增或单调递减。单调栈并不是一个Java集合中现成的容器,而是需要在编码中手动控制栈内元素的单调性。举个例子:有一个栈,用右边这个数组(右端是栈顶)来表示:【5,3,2,1】,目前来看,栈内的元素从栈底到栈顶是单调递减的,满足单调栈的属性,现在又来了一个4,那么这个4如果直接入栈,就会破坏单调栈的属性,为了保持单调,就需要依次将栈顶的1,2,3出栈,再将4入栈,此时栈内元素是【5,4】,依然满足单调栈的属性。所以说,栈内元素的单调性是人为保证的,那就是:当遇到要入栈的元素破坏了栈内的单调性,就需要将栈内的元素出栈一部分,直到满足单调性。

很多网上的概念以栈顶到栈底的递增或递减来定义单调递增栈或单调递减栈,有的相反。其实大可不必死记这些概念,以实际情况来保证是递增还是递减。

最后,来看下单调栈的使用场景:单调栈专门用来优化一种场景,就是快速找到数组中比当前元素大或者小的元素。额外使用了一个栈结构,将算法的时间复杂度由O(n2)降到O(n),典型的以空间换时间。至于怎么优化的,接下来通过LeetCode几道题来看。

正文

1、LeetCode 503. 下一个更大元素 II

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

该题使用暴力双重循环也能解决问题,但是时间复杂度为O(N2),我们可以使用单调栈的性质将时间复杂度降到O(N),我们以示例num2 = 【1,3,4,2】来模拟单调栈的操作(从栈底到栈顶依次递减)。
先建一个栈,然后依次遍历数组中的元素:
a. 走到num2的元素1,此时栈为空,直接将元素1的下标0入栈,栈内元素是【0】;
b. 走到num2的元素3,此时栈不空,判断栈顶元素(下标0)对应的值1,跟3的大小关系,发现1比3小,1 需要出栈,因为不出栈,就将3入栈的话,就不符合从栈底到栈顶依次递减的要求了。1出栈的同时也说明了:对于1这个元素来说,就已经找到了第一个比它大的元素,那么我们可以将这个记录下来,比如map.put(0,3),表示下标0代表的值1的第一个比它大的元素是3。将3对应的下标1入栈,此时栈内元素是【1】;
c. 走到num2的元素4,此时栈不空,判断栈顶元素(下标1)对应的值3,跟4的大小关系,发现3比4小,3需要出栈,理由跟上一步一样。3出栈的同时也说明了:对于3来说,就已经找到了第一个比它大的元素,那么我们可以将这个结果记录下来,map.put(1,4),表示下标1代表的值3的第一个比它大的元素是4。将4对应的下标2入栈,此时栈内元素是【2】;
d. 走到num2的元素2,此时栈不空,判断栈顶元素(下标2)对应的值4,跟2的大小关系,发现2比4小,2对应的下标3可以直接入栈,此时栈内元素是【2,3】。

上面的过程就只需要遍历一次数组,就能找到每个元素第一个比它大(或小)的元素,就像上面的【1、3、4、2】这个数组,元素1、3都在遍历的过程中找到了比它大的元素了,而4和2没有找到,仍然停留在栈内,于是再依次将栈内元素出栈,并将它们对应map的value置为-1(依题目要求)。

为什么map中要以下标作为key呢?因为数组中的元素可能有重复的,用下标可以唯一标识。

将上述过程以代码的形式来展示如下,下面的就是单调栈的核心代码,后续所有有关单调栈的题目都是以这块代码为核心进行扩展,可作为模板代码记下来。

Deque<Integer> stack = new ArrayDeque<>();
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
    while(!stack.empty() && nums[stack.peek()] < nums[i]) {
        map.put(stack.pop(), nums[i]);
    }
    stack.push(i);
}
while (!stack.empty()) {
    map.put(stack.pop(), -1);
}

于是,503这道题目的解答就是:

class Solution {
    public int[] nextGreaterElements(int[] nums1) {
        int[] ret = new int[nums1.length];

        int[] nums = new int[2 * nums1.length];
        for (int i = 0; i < nums1.length; i++) {
            nums[i] = nums1[i];
            nums[i + nums1.length] = nums1[i];
        }

        Deque<Integer> stack = new ArrayDeque<>();
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            while(!stack.empty() && nums[stack.peek()] < nums[i]) {
                map.put(stack.pop(), nums[i]);
            }
            stack.push(i);
        }
        while (!stack.empty()) {
            map.put(stack.pop(), -1);
        }

        
        for (int i = 0; i < nums1.length; i++) {
            ret[i] = map.get(i);
        }
        return ret;
    }
}

2、LeetCode 739. 每日温度

请根据每日 气温 列表 temperatures ,请计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。

这道题目也是经典的单调栈的问题,即找出数组中元素左边(或右边)第一个比它大(或小)的元素。那么立刻需要考虑使用单调栈来优化时间复杂度。按照上面提到的模板代码进行扩展:

class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        Deque<Integer> stack = new ArrayDeque<>();
        Map<Integer, Integer> map = new HashMap<>();

        for (int i = 0; i < temperatures.length; i++) {
            while (!stack.empty() && temperatures[stack.peek()] < temperatures[i]) {
                map.put(stack.pop(), i);
            }
            stack.push(i);
        }
        while (!stack.empty()) {
            map.put(stack.pop(), 0);
        }

		int[] ret = new int[temperatures.length];
        for (int i = 0; i < ret.length; i++) {
            if (map.get(i) == 0) {
                ret[i] = 0;
                continue;
            }
            ret[i] = map.get(i) - i;
        }
        return ret;
    }
}

3、LeetCode 42. 接雨水

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

算法学习(4):LeetCode刷题之单调栈_第1张图片

这道题目非常经典,如果用暴力的话,时间复杂度较高,同样考虑使用单调栈。前面的题目在进行单调栈操作的时候,每次都是拿栈顶元素和当前元素进行比较。而这道题目每次需要取出栈内2个元素和当前元素进行比较,能接住雨水的条件是:3个元素从左到右能组成一个凹槽。具体代码如下:

class Solution {
    public int trap(int[] height) {
        int ret = 0;
        Deque<Integer> stack = new ArrayDeque<>();
        for (int i = 0; i < height.length; i++) {
            while (!stack.empty() && height[stack.peek()] < height[i]) {
                int mid = stack.pop();
                if (stack.empty()) {
                    break;
                }
                int left = stack.peek();
                int right = i;
                // 每组成一次凹槽,就要计算一次接住的雨水,为了避免重复计算面积,高度需要每次减去mid位置的高度。
                ret += (right - left - 1) * (Math.min(height[left], height[right]) - height[mid]);
            }
            stack.push(i);
        }
        return ret;
    }
}

4、LeetCode 84. 柱状图中最大的矩形

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

算法学习(4):LeetCode刷题之单调栈_第2张图片
这道题目如果是用暴力破解的话,遍历每一根柱子,然后以这跟柱子为中心向两边扩展,直到找到第一个比当前柱子低的柱子,然后计算面积,循环中进行比较。这样就需要双重循环,时间复杂度为O(n2)。怎么优化呢?上面我们提到了一句话,“直到找到第一个比当前柱子低的柱子”,那么就要想起使用单调栈,我们先找出每一根柱子左边(及右边)第一个比它低的柱子,存到map备用。最后循环一遍数组,然后从map中拿到每根柱子左右两边第一个比它低的,然后计算面积,这样一来,时间复杂度降为了O(3n)。详细代码如下:

class Solution {
    public int largestRectangleArea(int[] heights) {
        Deque<Integer> stack = new ArrayDeque<>();
        Map<Integer, Integer> left = new HashMap<>();  // 记录每根柱子左边第一个比它低的柱子
        Map<Integer, Integer> right = new HashMap<>(); // 记录每根柱子右边第一个比它低的柱子

		// 先填充left
        for (int i = 0; i < heights.length; i++) {
            while (!stack.isEmpty() && heights[i] < heights[stack.peek()]) {
                right.put(stack.pop(), i);
            }
            stack.push(i);
        }
        while (!stack.isEmpty()) {
            right.put(stack.pop(), heights.length);
        }
        // 再填充right
        for (int i = heights.length - 1; i >= 0; i--) {
            while (!stack.isEmpty() && heights[i] < heights[stack.peek()]) {
                left.put(stack.pop(), i);
            }
            stack.push(i);
        }
        while (!stack.isEmpty()) {
            left.put(stack.pop(), -1);
        }

		// 最后遍历一遍数组
        int ret = 0;
        for (int i = 0; i < heights.length; i++) {
            ret = Math.max(ret, heights[i] * (right.get(i) - left.get(i) - 1));
        }
        return ret;
    }
}

总结

单调栈专门用来优化一种场景,那就是找到数组中每个元素左边(或右边)第一个比它大(或小)的元素。上面的代码作为模板来记。

你可能感兴趣的:(算法,算法,java,单调栈,leetcode,接雨水)