羊羊刷题笔记Day58/60 | 第十章 单调栈P1 | 739. 每日温度、496. 下一个更大元素

53 阅读6分钟

739 每日温度

为什么用单调栈

怎么能想到用单调栈呢? 什么时候用单调栈呢?

作用

通常是一维数组,要寻找任一个元素的 (右边 / 左边) 第一个比自己 (大 / 小) 的元素的位置,此时我们就要想到可以用单调栈了。时间复杂度为O(n)。
例如本题其实就是找找到一个元素右边第一个比自己大的元素,此时就应该想到用单调栈了。
那么单调栈的原理是什么呢?为什么时间复杂度是O(n)就可以找到每一个元素的右边第一个比它大的元素位置呢?

原理

单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。
更直白来说,就是用一个栈来记录我们遍历过的元素,因为我们遍历数组的时候,我们不知道之前都遍历了哪些元素,以至于遍历一个元素找不到是不是之前遍历过一个更小的,所以我们需要用一个容器(这里用单调栈)来记录我们遍历过的元素。

如何使用

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

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

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

  1. 单调栈里元素是递增呢? 还是递减呢?


注意以下顺序的描述为 从栈头到栈底的顺序,因为单纯的说从左到右或者从前到后,不说栈头朝哪个方向的话,一定比较懵。(上图为栈口朝左 即栈底在最右边的情况
这里我们要使用递增循序(再强调一下是指从栈头到栈底的顺序),因为只有递增的时候,栈里要加入一个元素i的时候,才知道栈顶元素在数组中右面第一个比栈顶元素大的元素是i。
(规律:如果求一个元素右边第一个更大元素,单调栈就是递增的,如果求一个元素右边第一个更小元素,单调栈就是递减的。

无论是求最近的较大或较小值,即在单调栈中顺序为递增或递减,使用单调栈主要考虑都是这三个判断条件。

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

把这三种情况分析清楚了,也就理解透彻了


理解单调栈最好的办法就是自己模拟以便过程,图文过程详见


通过模拟,就会发现:只有单调栈递增(从栈口到栈底顺序),就是求右边第一个比自己大的,单调栈递减的话,就是求右边第一个比自己小的。
代码如下:

public int[] dailyTemperatures(int[] temperatures) {
    LinkedList<Integer> st = new LinkedList<>();
    int[] result = new int[temperatures.length];

    st.push(0);
    
    for (int i = 1; i < temperatures.length; i++) {
        if (!st.isEmpty() && temperatures[i] <= temperatures[st.peek()])
            // 进栈元素小
            st.push(i);
        else {
            // 进栈元素大
            while (!st.isEmpty() && temperatures[i] > temperatures[st.peek()]) {
                result[st.peek()] = i - st.peek();
                st.pop();
            }
            // 进栈元素入栈
            st.push(i);
        }

    }
    return result;

}

精简代码如下:

public int[] dailyTemperatures(int[] temperatures) {
    LinkedList<Integer> st = new LinkedList<>();
    int[] result = new int[temperatures.length];

    st.push(0);
    for (int i = 1; i < temperatures.length; i++) {
        // 简化版
        while (!st.isEmpty() && temperatures[i] > temperatures[st.peek()]) {
            result[st.peek()] = i - st.peek();
            st.pop();
        }
        st.push(i);

    }
    return result;

}
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

精简的代码是直接把情况一二三都合并到了一起,其实这种代码精简是精简,但思路不是很清晰。
建议把情况一二三想清楚了,先写出版本一的代码,然后在其基础上在做精简!

496 下一个更大元素

✍手写分析

image.png

思路

从题目示例中我们可以看出最后是要求nums1的每个元素在nums2中下一个比当前元素大的元素,那么就要定义一个和nums1一样大小的数组result来存放结果。
题目说如果不存在对应位置就输出 -1 ,所以result数组如果某位置没有被赋值,那么就应该是是-1,所以就初始化为-1。
在遍历nums2的过程中,我们要判断nums2[i]是否在nums1中出现过,因为最后是要根据nums1元素的下标来更新result数组。
注意题目中说是两个没有重复元素 的数组 nums1 和 nums2
没有重复元素,我们就可以用map来做映射。根据数值快速找到下标,还可以判断nums2[i]是否在nums1中出现过。
那么映射nums1预处理代码如下:

// 映射nums1
HashMap<Integer, Integer> hm = new HashMap<>();
for (int i = 0; i < nums1.length; i++) {
    hm.put(nums1[i], i);
}

使用单调栈,首先要想单调栈是从大到小还是从小到大。
本题和739. 每日温度是一样的。
栈头到栈底的顺序,要从小到大,也就是保持栈里的元素为递增顺序。只要保持递增,才能找到右边第一个比自己大的元素。
接下来就要分析如下三种情况,一定要分析清楚。

  1. 情况一:当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况

此时满足递增栈(栈头到栈底的顺序),所以直接入栈。

  1. 情况二:当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况

如果相等的话,依然直接入栈,因为我们要求的是右边第一个比自己大的元素,而不是大于等于!

  1. 情况三:当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况

此时如果入栈就不满足递增栈了,这也是找到右边第一个比自己大的元素的时候。
判断栈顶元素是否在nums1里出现过,(注意栈里的元素是nums2的元素),如果出现过,开始记录结果。
代码如下:

while (!st.isEmpty() && nums2[i] > nums2[st.peek()]){
    // 尝试给num1里的值赋值
    if (hm.containsKey(nums2[st.peek()])){
        Integer index = hm.get(nums2[st.peek()]);
        result[index] = nums2[i];
    }
    st.pop();
}
st.push(i);

以上分析完毕,代码如下:(其实本题代码和 739. 每日温度是基本差不多的)

public int[] nextGreaterElement(int[] nums1, int[] nums2) {
    // 映射nums1
    HashMap<Integer, Integer> hm = new HashMap<>();
    for (int i = 0; i < nums1.length; i++) {
        hm.put(nums1[i], i);
    }

    // 初始化result数组
    int[] result = new int[nums1.length];
    Arrays.fill(result, -1);

    LinkedList<Integer> st = new LinkedList();
    st.push(0);

    for (int i = 1; i < nums2.length; i++) {
        if (!st.isEmpty() && nums2[i] <= nums2[st.peek()])
            st.push(i);
        else {
            while (!st.isEmpty() && nums2[i] > nums2[st.peek()]){
                // 尝试给num1里的值赋值
                if (hm.containsKey(nums2[st.peek()])){
                    Integer index = hm.get(nums2[st.peek()]);
                    result[index] = nums2[i];
                }
                st.pop();
            }
            st.push(i);
        }

    }
    return result;
}

针对版本一,进行代码精简后,代码如下:

public int[] nextGreaterElement(int[] nums1, int[] nums2) {
            // 映射nums1
            HashMap<Integer, Integer> hm = new HashMap<>();
            for (int i = 0; i < nums1.length; i++) {
                hm.put(nums1[i], i);
            }

            // 初始化result数组
            int[] result = new int[nums1.length];
            Arrays.fill(result, -1);

            LinkedList<Integer> st = new LinkedList();
            st.push(0);

            for (int i = 1; i < nums2.length; i++) {
                
                // 简洁版
                while (!st.isEmpty() && nums2[i] > nums2[st.peek()]) {
                    // 尝试给num1里的值赋值
                    if (hm.containsKey(nums2[st.peek()])) {
                        Integer index = hm.get(nums2[st.peek()]);
                        result[index] = nums2[i];
                    }
                    st.pop();
                }
                st.push(i);

            }
            return result;
        }

精简的代码是直接把情况一二三都合并到了一起,其实这种代码精简是精简,但思路不是很清晰。
建议把情况一二三想清楚了,先写出版本一的代码,然后在其基础上在做精简!

学习资料:

739. 每日温度

496.下一个更大元素