羊羊刷题笔记Day 59,60/60 | 第十章 单调栈P1 | 503. 下一个更大元素II、42. 接雨水、84. 柱状图中最大的矩形

115 阅读8分钟

503.下一个更大元素II

本题与 496 下一个更大元素 区别就是将数组变成了循环数组

思路

如何处理循环数组?
简单粗暴的方法:直接把两个数组拼接在一起,然后使用单调栈求下一个最大值不就行了!
将两个nums数组拼接在一起,使用单调栈计算出每一个元素的下一个最大值,最后再把结果集即result数组resize到原数组大小就可以了。

这种写法确实比较直观,但做了很多无用操作,例如修改了nums数组,而且最后还要把result数组resize回去。
resize倒是不费时间,是O(1)的操作,但扩充nums数组相当于多了一个O(n)的操作。


其实也可以不扩充nums,而是使用取模思想,在遍历的过程中模拟走了两遍nums。
代码如下:

public int[] nextGreaterElements(int[] nums) {
    LinkedList<Integer> st = new LinkedList();

    int[] result = new int[nums.length];
    Arrays.fill(result,-1);

    st.push(0);
    // 终止条件为两次length
    for (int i = 1; i < nums.length * 2; i++){
        // 循环内的下标取模,模拟走两变
        if (nums[i % nums.length] <= nums[st.peek() % nums.length]){
            st.push(i % nums.length);
        }
        else {
            while (!st.isEmpty() && nums[i % nums.length] > nums[st.peek() % nums.length]){
                result[st.peek() % nums.length] = nums[i % nums.length];
                st.pop();
            }
            st.push(i % nums.length);
        }
    }

    return result;

}

当然也可以像昨天的题一样精简代码,但一定是要根据思想写出了这一版本的代码后再此基础上精简代码,思路才能明确

42. 接雨水

这里只介绍单调栈的方法,其他方法详见

单调栈就是保持栈内元素有序。
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。
而接雨水这道题目,我们正需要寻找一个元素,右边最大元素以及左边最大元素,来计算雨水面积。

准备工作

那么本题使用单调栈有如下几个问题:

  1. 首先单调栈是按照行方向来计算雨水,如图:


知道这一点,后面的就可以理解了。

  1. 使用单调栈内元素的顺序

从大到小还是从小到大呢?

为了制造凹槽,从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。
因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。
如图:

还是之前所说的规律:
739. 每日温度中求一个元素右边第一个更大元素,单调栈就是递增的
求一个元素右边第一个更小元素,单调栈就是递减的。(在下一题)

  1. 遇到相同高度的柱子怎么办。

遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。
例如 5 5 1 3 这种情况。如果添加第二个5的时候就应该将第一个5的下标弹出,把第二个5添加到栈中。
因为我们要求宽度的时候 如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度
如图所示:

  1. 栈里要保存什么数值

使用单调栈,也是通过 长 * 宽 来计算雨水面积的。
长就是通过柱子的高度来计算,宽是通过柱子之间的下标来计算,
所以栈的定义如下:

LinkedList<Integer>  st = new LinkedList<>();

明确了如上几点,我们再来看处理逻辑。

单调栈处理逻辑

以下逻辑主要就是三种情况

  • 情况一:当前遍历的元素(柱子)高度小于栈顶元素的高度 height[i] < height[st.top()]
  • 情况二:当前遍历的元素(柱子)高度等于栈顶元素的高度 height[i] == height[st.top()]
  • 情况三:当前遍历的元素(柱子)高度大于栈顶元素的高度 height[i] > height[st.top()]

先将下标0的柱子加入到栈中,st.push(0);。 栈中存放我们遍历过的元素,所以先将下标0加进来。
然后开始从下标1开始遍历所有的柱子,for (int i = 1; i < height.size(); i++)。
如果当前遍历的元素(柱子)高度小于栈顶元素的高度,就把这个元素加入栈中,因为栈里本来就要保持从小到大的顺序(从栈头到栈底)。
代码如下:

if (height[i] < height[st.peek()])
    st.push(i);

如果当前遍历的元素(柱子)高度等于栈顶元素的高度,要跟更新栈顶元素,因为遇到相相同高度的柱子,需要使用最右边的柱子的下标来计算宽度
代码如下:

else if (height[i] == height[st.peek()]) {
    st.pop();
    st.push(i);
}

如果当前遍历的元素(柱子)高度大于栈顶元素的高度,此时就出现凹槽了,如图所示:

取栈顶元素,将栈顶元素弹出,这个就是凹槽的底部,也就是中间位置,下标记为mid
此时的栈顶元素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。
求当前凹槽雨水的体积代码如下:

// while循环通过左右比中间高的柱子计算底高,从而计算面积
while (!st.isEmpty() && height[i] > height[st.peek()]){
    int mid = st.pop();
    if (!st.isEmpty()){
        // 高 - 用到最后第二个元素
        int h = Math.min(height[st.peek()],height[i]) - height[mid];
        // 宽
        int w = i - st.peek() - 1;
        // 面积
        int s = h * w;
        result += s;
    }
}

关键部分讲完了,整体代码如下:

public int trap(int[] height) {
    int result = 0;
    LinkedList<Integer>  st = new LinkedList<>();

    st.push(0);
    for (int i = 1; i < height.length; i++){
        if (height[i] < height[st.peek()])
            st.push(i);
        else if (height[i] == height[st.peek()]) {
            st.pop();
            st.push(i);
        }
        else {
            // while循环通过左右比中间高的柱子计算底高,从而计算面积
            while (!st.isEmpty() && height[i] > height[st.peek()]){
                int mid = st.pop();
                if (!st.isEmpty()){
                    // 高 - 用到最后第二个元素
                    int h = Math.min(height[st.peek()],height[i]) - height[mid];
                    // 宽
                    int w = i - st.peek() - 1;
                    // 面积
                    int s = h * w;
                    result += s;
                }
            }
            st.push(i);
        }

    }
    return result;
}

代码同样可以精简,但精简后就看不出去三种情况的处理了,不太利于理解。

84.柱状图中最大的矩形

这里只介绍单调栈方法,其他(暴力、双指针)详见

本地单调栈的解法和接雨水的题目是遥相呼应的。
上一题 42. 接雨水 是找每个柱子左右两边第一个大于该柱子高度的柱子,而本题是找每个柱子左右两边第一个小于该柱子的柱子

思路

因此这里就涉及到了单调栈很重要的性质,就是单调栈里的顺序,是从小到大还是从大到小
在题解 42. 接雨水 中接雨水的单调栈从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。
那么因为本题是要找每个柱子左右两
边第一个小于该柱子的柱子
,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序!即元素大的才能进栈

只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。


所以本题单调栈的顺序正好与接雨水反过来。
此时应该可以发现其实就是栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度
理解这一点,对单调栈就掌握的比较到位了。

主要就是分析清楚如下三种情况:

  • 情况一:当前遍历的元素heights[i]大于栈顶元素heights[st.top()]的情况
  • 情况二:当前遍历的元素heights[i]等于栈顶元素heights[st.top()]的情况
  • 情况三:当前遍历的元素heights[i]小于栈顶元素heights[st.top()]的情况

代码如下:

public int largestRectangleArea(int[] heights) {
    // 数组前后加0
    int[] nums = new int[heights.length + 2];
    nums[0] = 0;
    nums[heights.length + 1] = 0;
    for (int i = 0; i < heights.length; i++) {
        nums[i + 1] = heights[i];
    }

    LinkedList<Integer> st = new LinkedList<>();

    // 单调栈思路
    st.push(0);
    int result = 0;
    for (int i = 1; i < nums.length; i++){
        if (nums[i] > nums[st.peek()]){
            st.push(i);
        } else if (nums[i] == nums[st.peek()]) {
            st.pop();
            st.push(i);
        } else {

            // 插入了值比栈顶元素小 - 出现了右边界 - 用while寻找左边界算面积
            while (!st.isEmpty() && nums[i] < nums[st.peek()]){
                Integer mid = st.pop();
                if (!st.isEmpty()){
                    // 区别体现:需要在弹出栈时做面积计算
                    int left = st.peek();
                    int right = i;
                    // 底
                    int d = right - left - 1;
                    // 高 - 高为mid 由于前面加了0 所以+1
                    int h = nums[mid];
                    // 面积
                    int s = d * h;
                    result = Math.max(s,result);
                }
            }
            st.push(i);
        }
    }

    return result;
}

观察代码发现,为什么在数组前后加上0呢?

  • 末尾为什么要加元素0?

如果数组本身就是升序的,例如[2,4,6,8],那么入栈之后 都是单调递减,一直都没有走 情况三 计算结果的哪一步,所以最后输出的就是0了。 如图:

那么结尾加一个0,就会让栈里的所有元素,走到情况三的逻辑。

  • 开头为什么要加元素0?

如果数组本身是降序的,例如 [8,6,4,2],在 8 入栈后,6 开始与8 进行比较,此时我们得到 mid(8),rigt(6),但是得不到 left。
(mid、left,right 都是对应版本一里的逻辑)
因为 将 8 弹出之后,栈里没有元素了,那么为了避免空栈取值,直接跳过了计算结果的逻辑。
之后又将6 加入栈(此时8已经弹出了),然后 就是 4 与 栈口元素 8 进行比较,周而复始,那么计算的最后结果resutl就是0。 如图所示:

所以我们需要在 height数组前后各加一个元素0。
同样代码可以精简,但直接看会忽略细节。

学习资料:

503.下一个更大元素II

42. 接雨水

84.柱状图中最大的矩形