C++ 数据结构与算法(十三)(单调栈)

单调栈

单调栈就是栈内元素成通过维护保持一定顺序的栈结构,

通常是一维数组中要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。

单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录递增或递减的元素,优点是只需要遍历一次。

739. 每日温度 ●●

给定一个整数数组 Ts ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

1. 暴力遍历

从前往后,两层 for 循环,找到第一个更大的元素下标进行比较计算,时间复杂度为 O ( n 2 ) O(n^2) O(n2)

2. 反向遍历

题目中温度范围在[30, 100]度之间,从后往前遍历,维护一个数组 next 记录当前温度下的最小索引,同时查找右侧更高温度的最小索引 warmerIndex 并记录。

  • 时间复杂度: O ( n m ) O(nm) O(nm),其中 n 是温度列表的长度,m 是数组 next 的长度,在本题中温度不超过 100。反向遍历温度列表一遍,对于温度列表中的每个值,都要遍历数组 next 一遍。
  • 空间复杂度: O ( m ) O(m) O(m),其中 m 是数组 next 的长度。除了返回值以外,需要维护长度为 m 的数组 next 记录每个温度第一次出现的下标位置。
class Solution {
    
    
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
    
    
        int n = temperatures.size();
        vector<int> ans(n), next(101, INT_MAX);
        for (int i = n - 1; i >= 0; --i) {
    
    
            int warmerIndex = INT_MAX;
            for (int t = temperatures[i] + 1; t <= 100; ++t) {
    
    
                warmerIndex = min(warmerIndex, next[t]);
            }
            if (warmerIndex != INT_MAX) {
    
    
                ans[i] = warmerIndex - i;
            }
            next[temperatures[i]] = i;
        }
        return ans;
    }
};

3. 单调栈

维护一个存储下标的单调栈,从栈顶到栈底的下标对应的温度列表中的温度依次递增

如果一个下标在单调栈里,则表示尚未找到下一次温度更高的下标。

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

  1. 当前遍历的元素 T[i] 大于栈顶元素 T[st.top()] 的情况:
    此时,当前的元素 T[i] 即为栈顶元素右边第一个更大的温度,因此将栈顶弹出,并计算等待天数;循环判断,直到栈的单调性成立;然后将该元素下标压入栈;

  2. 当前遍历的元素 T[i] 等于栈顶元素 T[st.top()] 的情况:
    元素下标直接入栈,等待更高的温度来打破栈的单调性;

  3. 当前遍历的元素 T[i] 小于栈顶元素 T[st.top()] 的情况:
    元素下标直接入栈,等待更高的温度来打破栈的单调性;

  • 时间复杂度: O ( n ) O(n) O(n),其中 n 是温度列表的长度。正向遍历温度列表一遍,对于温度列表中的每个下标,最多有一次进栈和出栈的操作。
  • 空间复杂度: O ( n ) O(n) O(n),其中 n 是温度列表的长度。需要维护一个单调栈存储温度列表中的下标。
class Solution {
    
    
public:
    vector<int> dailyTemperatures(vector<int>& Ts) {
    
    
        stack<int> st;
        int n = Ts.size();
        vector<int> ans(n, 0);
        for(int i = 0; i < n; ++i){
    
    
            while(!st.empty() && Ts[i] > Ts[st.top()]){
    
    		// 维护单调栈
                ans[st.top()] = i - st.top();
                st.pop();
            }
            st.push(i);    		// 栈内没有元素,或 入栈后单调性不变,下标直接入栈
        }
        return ans;
    }
};

496. 下一个更大元素 I ●

nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。

给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中 nums1 是 nums2 的子集

对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1

返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。

输入:nums1 = [4,1,2], nums2 = [1,3,4,2].
输出:[-1,3,-1]
解释:nums1 中每个值的下一个更大元素如下所述:

  • 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
  • 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。
  • 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。

1. 暴力遍历

2. 单调栈 + 哈希

为了找到右边第一个更大的元素,维护从栈顶至栈底元素值递增的单调栈,因此当出现更大的元素打破单调性时,栈内的元素即找到了右边第一个更大的元素

首先遍历 nums1,将元素值与下标做映射,能够判断 nums2 的某些元素在 nums1 是否存在,同时也找到直接修改的对应的数组下标。

class Solution {
    
    
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
    
    
        int n1 = nums1.size(), n2 = nums2.size();
        vector<int> ret(n1, -1);
        stack<int> st;
        unordered_map<int, int> map;			// 哈希下标映射
        for(int i = 0; i < n1; ++i) map[nums1[i]] = i;
        for(int i = 0; i < n2; ++i){
    
    
            while(!st.empty() && st.top() < nums2[i]){
    
    		// 打破单调性
                if(map.count(st.top()) > 0) ret[map[st.top()]] = nums2[i];
                st.pop();
            }
            st.push(nums2[i]);					// 符合单调性,直接入栈
        }
        return ret;
    }
};

496. 下一个更大元素 II ●

给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。

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

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

1. 单调栈

我们注意到,暴力解法中,如果数组的前半部分是单调不增的,那么会有很大的计算资源的浪费。比如说 [6,5,4,3,8],对于前面的 [6,5,4,3] 等数字都需要向后遍历,当寻找到元素 8 时才找到了比自己大的元素;而如果已知元素 6 向后找到元素 8 才找到了比自己的大的数字,那么对于元素 [5,4,3] 来说,它们都比元素 6 更小,所以比它们更大的元素一定是元素 8,不需要单独遍历对 [5,4,3] 向后遍历一次!

根据上面的分析可知,可以遍历一次数组,如果元素是单调递减的(则他们的**「下一个更大元素」相同**),我们就把这些元素保存,直到找到一个较大的元素;把该较大元素逐一跟保存了的元素比较,如果该元素更大,那么它就是前面元素的「下一个更大元素」。

在这里插入图片描述
循环数组两种常见的实现方式:

  1. 把数组复制一份到数组末尾,这样虽然不是严格的循环数组,但是对于本题已经足够了,因为本题对数组最多遍历两次。
  2. 使用 取模运算 % 可以把下标 i 映射到数组 nums 长度的 0−N 内。
class Solution {
    
    
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
    
    
        int n = nums.size();
        vector<int> ret(n, -1);
        stack<int> st;
        for(int i = 0; i < 2*n-1; ++i){
    
    
            while(!st.empty() && nums[i%n] > nums[st.top()%n]){
    
    	// 单调性破坏,找到更大值
                ret[st.top()%n] = nums[i%n];
                st.pop();
            }
            st.push(i);		// 栈中保存元素下标
        }
        return ret;
    }
};

猜你喜欢

转载自blog.csdn.net/qq_19887221/article/details/126709070