单调栈-处理Next Greater Element问题

单调栈有两种处理框架

如:下一个更大元素

1, 从右往左遍历,维护单调递增栈(想象:栈顶朝左, i--的方向),在内层while循环外记录res(因此对每个元素是实时更新res), 内存循环出栈目的是扫清障碍(因此不满足单调性时仅循环出栈,不能在循环内更新res)
栈内元素的意义栈内记录的是当前元素的结果. 对当前元素来说,栈顶就是它的res
(1)当前元素若满足单调性的元素,更新res,当前元素直接入栈
(2)当前元素若会打破栈的单调性,则循环出栈(目的是扫清障碍),循环结束后更新res,当前元素入栈。

    // 单调递增栈(栈顶朝左, i--的方向):栈内记录的是当前元素的结果. 对当前元素来说,栈顶就是它的res
    vector<int> nextGreaterElement(vector<int>& nums) {
        vector<int> ans(nums.size());
        stack<int> s;
        for (int i = nums.size() - 1; i >= 0; i--) {
            while (!s.empty() && nums[i] >= s.top()) { // (2)当前元素会打破单调性,仅循环出栈(扫清障碍),判断条件需要带等号(保证栈内元素不重复)
                s.pop();
            }
            ans[i] = s.empty() ? -1 : s.top(); // 对每个元素都是实时更新res
            s.push(nums[i]); // (1)当前元素入栈
        }
        return ans;
    }

2, 从左往右遍历,维护单调递增栈(想象:栈顶朝右, i++的方向),在内层while循环内记录res(因此对每个元素可能会延时更新res)
栈内元素的意义栈内记录的之前的元素. 当前元素打破单调性时,告诉栈内的元素,你们出来吧,我当前元素就是你们的结果
(1)当前元素若满足单调性的元素,直接入栈
(2)当前元素若会打破栈的单调性,则循环出栈并对出栈的元素更新res。
(3)因为每个元素只有在其后面的元素打破单调性时才会被更新到res,存在延时更新问题。因此需要特殊处理双层循环后仍存在栈中的元素。

(4)因为元素存在延时更新res问题,因此栈内有可能记录的是索引

class Solution1 {
public:
    // 单调递增栈(栈顶朝右, i++的方向):栈内记录的之前的元素. 当前元素打破单调性时,告诉栈内的元素,你们出来吧,我当前元素就是你们的结果
    vector<int> nextGreaterElement(vector<int>& nums) {
        vector<int> ans(nums.size(), -1); // (3) 因为存在延时更新,因此特殊初始化为符合结果要求的-1
        stack<int> s;
        for (int i = 0; i < nums.size() - 1; i++) {
            while (!s.empty() && nums[i] > nums[s.top()]) { // (2)当前元素打破单调性时,循环更新之前元素的res。判断条件不带=号(保证记录全所有元素)
                ans[s.top()] = nums[i];
                s.pop();
            }
            s.push(i); // (1)因为对元素存在延时更新res,只能记录索引
        }
        return ans;
    }
};

转载

未找到网页 - labuladong 的算法小抄

思路:

(1) 倒序遍历

(2) 对遍历过程的当前元素, 把当前栈中比当前元素小的元素都拉出栈(单调递增栈)

(3) 经过第(2)步后,栈顶的元素就是处于当前元素后第一个比当前元素值大的元素。栈顶元素写入结果数组对应位置

(4) 把倒序遍历当前元素入栈,准备接受下一个遍历元素的审判

注意:若是循环数组,有两点注意. (1)假想出双倍长度的数组 (2)不管处理从原始数组中取数据还是写入到目的数组,都要用取模方式处理某点左边数组和右边数组

栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈。

单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。

听起来有点像堆(heap)?不是的,单调栈用途不太广泛,只处理一种典型的问题,叫做 Next Greater Element。本文用讲解单调队列的算法模版解决这类问题,并且探讨处理「循环数组」的策略。

首先,讲解 Next Greater Number 的原始问题:给你一个数组,返回一个等长的数组,对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。不好用语言解释清楚,直接上一个例子:

给你一个数组 [2,1,2,4,3],你返回数组 [4,2,4,-1,-1]。

解释:第一个 2 后面比 2 大的数是 4; 1 后面比 1 大的数是 2;第二个 2 后面比 2 大的数是 4; 4 后面没有比 4 大的数,填 -1;3 后面没有比 3 大的数,填 -1。

这道题的暴力解法很好想到,就是对每个元素后面都进行扫描,找到第一个更大的元素就行了。但是暴力解法的时间复杂度是 O(n^2)。

这个问题可以这样抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「2」的 Next Greater Number 呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的 Next Greater Number,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。

这个情景很好理解吧?带着这个抽象的情景,先来看下代码。

vector<int> nextGreaterElement(vector<int>& nums) {
    vector<int> ans(nums.size()); // 存放答案的数组
    stack<int> s;
    // (1) 倒序遍历
    for (int i = nums.size() - 1; i >= 0; i--) { // 倒着往栈里放
        // (2) 对遍历过程的当前元素, 把当前栈中比当前元素小的元素都拉出栈
        while (!s.empty() && s.top() <= nums[i]) { // 判定个子高矮
            s.pop(); // 矮个起开,反正也被挡着了。。。
        }
        // (3) 经过第(2)步后,栈顶的元素就是处于当前元素后第一个比当前元素值大的元素。栈顶元素写入结果数组对应位置
        ans[i] = s.empty() ? -1 : s.top(); // 这个元素身后的第一个高个
        // (4) 把倒序遍历当前元素入栈, 准备接受下一个遍历元素的审判
        s.push(nums[i]); // 进队,接受之后的身高判定吧!
    }
    return ans;
}

这就是单调队列解决问题的模板。for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。while 循环是把两个“高个”元素之间的元素排除,因为他们的存在没有意义,前面挡着个“更高”的元素,所以他们不可能被作为后续进来的元素的 Next Great Number 了。

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

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

现在,你已经掌握了单调栈的使用技巧,来一个简单的变形来加深一下理解。

给你一个数组 T = [73, 74, 75, 71, 69, 72, 76, 73],这个数组存放的是近几天的天气气温(这气温是铁板烧?不是的,这里用的华氏度)。你返回一个数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0 。

举例:给你 T = [73, 74, 75, 71, 69, 72, 76, 73],你返回 [1, 1, 4, 2, 1, 1, 0, 0]。

解释:第一天 73 华氏度,第二天 74 华氏度,比 73 大,所以对于第一天,只要等一天就能等到一个更暖和的气温。后面的同理。

你已经对 Next Greater Number 类型问题有些敏感了,这个问题本质上也是找 Next Greater Number,只不过现在不是问你 Next Greater Number 是多少,而是问你当前距离 Next Greater Number 的距离而已。

相同类型的问题,相同的思路,直接调用单调栈的算法模板,稍作改动就可以啦,直接上代码把。

vector<int> dailyTemperatures(vector<int>& T) {
    vector<int> ans(T.size());
    stack<int> s; // 这里放元素索引,而不是元素
    for (int i = T.size() - 1; i >= 0; i--) {
        while (!s.empty() && T[s.top()] <= T[i]) {
            s.pop();
        }
        ans[i] = s.empty() ? 0 : (s.top() - i); // 得到索引间距
        s.push(i); // 加入索引,而不是元素
    }
    return ans;
}

单调栈讲解完毕。下面开始另一个重点:如何处理「循环数组」。

同样是 Next Greater Number,现在假设给你的数组是个环形的,如何处理?

给你一个数组 [2,1,2,4,3],你返回数组 [4,2,4,-1,4]。拥有了环形属性,最后一个元素 3 绕了一圈后找到了比自己大的元素 4 。

首先,计算机的内存都是线性的,没有真正意义上的环形数组,但是我们可以模拟出环形数组的效果,一般是通过 % 运算符求模(余数),获得环形特效:

int[] arr = {1,2,3,4,5};
int n = arr.length, index = 0;
while (true) {
    print(arr[index % n]);
    index++;
}

回到 Next Greater Number 的问题,增加了环形属性后,问题的难点在于:这个 Next 的意义不仅仅是当前元素的右边了,有可能出现在当前元素的左边(如上例)。

明确问题,问题就已经解决了一半了。我们可以考虑这样的思路:将原始数组“翻倍”,就是在后面再接一个原始数组,这样的话,按照之前“比身高”的流程,每个元素不仅可以比较自己右边的元素,而且也可以和左边的元素比较了。

怎么实现呢?你当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,我们可以不用构造新数组,而是利用循环数组的技巧来模拟。直接看代码吧:

循环数组,有两点注意:(1)假想出双倍长度的数组 (2)用取模方式处理某点左边数组和右边数组 

vector<int> nextGreaterElements(vector<int>& nums) {
    int n = nums.size();
    vector<int> res(n); // 存放结果
    stack<int> s;
    // 假装这个数组长度翻倍了
    for (int i = 2 * n - 1; i >= 0; i--) {
        while (!s.empty() && s.top() <= nums[i % n])
            s.pop();
        res[i % n] = s.empty() ? -1 : s.top();
        s.push(nums[i % n]);
    }
    return res;
}

练习:

496. 下一个更大元素 I

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        unordered_map<int, int> numBignumMap;
        vector<int> result(nums1.size());
        stack<int> stk;
        // (1)倒序遍历
        for (int i = nums2.size() - 1; i >= 0; i--) {
            // (2) 对遍历过程的当前元素, 把当前栈中比当前元素小的元素都拉出栈
            while (!stk.empty() && stk.top() <= nums2[i]) {
                stk.pop();
            }
            // (3) 经过第(2)步后,栈顶的元素就是处于当前元素后第一个比当前元素值大的元素
            int bigNum = stk.empty() == true ? -1 : stk.top();
            numBignumMap[nums2[i]] = bigNum;
            // (4) 倒序遍历入栈
            stk.push(nums2[i]);
        }
        for (size_t i = 0; i < nums1.size(); i++) {
            result[i] = numBignumMap[nums1[i]];
        }
        return result;
    }
};

503. 下一个更大元素 II

循环数组


    // 循环数组,有两点注意. (1)假想出双倍长度的数组 (2)用取模方式处理某点左边数组和右边数组
    vector<int> nextGreaterElements(vector<int>& nums) {
        size_t n = nums.size();
        vector<int> result(n);
        stack<int> stk;
        // 注意(1)假想出双倍长度的数组
        for (int i = n * 2 - 1; i >= 0; i--) {
            // 注意(2)用取模方式处理某点左边数组和右边数组
            while (!stk.empty() && stk.top() <= nums[i % n]) {
                stk.pop();
            }
            // 注意(2)用取模方式处理某点左边数组和右边数组
            result[i % n] = stk.empty() == true ? -1 : stk.top();
            // 注意(2)用取模方式处理某点左边数组和右边数组
            stk.push(nums[i % n]); 
        }
        return result;
    }

84. 柱状图中最大的矩形

思路:

对每一个元素,求出其左侧第一个比它小的元素位置,再求出其右侧第一个比它小的元素位置。

然后:以当前元素为中心可以得到的最大面积就是(right_first_small - left_first_small) * height[i]。

方法一:一遍从前往后框架,一遍从右往左框架:

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        int n = heights.size();
        vector<int> left(n), right(n);
        
        stack<int> mono_stack;
        for (int i = 0; i < n; ++i) {
            while (!mono_stack.empty() && heights[mono_stack.top()] >= heights[i]) {
                mono_stack.pop();
            }
            left[i] = (mono_stack.empty() ? -1 : mono_stack.top());
            mono_stack.push(i);
        }

        mono_stack = stack<int>();
        for (int i = n - 1; i >= 0; --i) {
            while (!mono_stack.empty() && heights[mono_stack.top()] >= heights[i]) {
                mono_stack.pop();
            }
            right[i] = (mono_stack.empty() ? n : mono_stack.top());
            mono_stack.push(i);
        }
        
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans = max(ans, (right[i] - left[i] - 1) * heights[i]);
        }
        return ans;
    }
};

 方法二:一遍遍历,两个框架思想结合

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        int n = heights.size();
        vector<int> left(n), right(n, n);
        
        stack<int> mono_stack;
        for (int i = 0; i < n; ++i) {
            while (!mono_stack.empty() && heights[mono_stack.top()] >= heights[i]) {
                right[mono_stack.top()] = i;
                mono_stack.pop();
            }
            left[i] = (mono_stack.empty() ? -1 : mono_stack.top());
            mono_stack.push(i);
        }
        
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans = max(ans, (right[i] - left[i] - 1) * heights[i]);
        }
        return ans;
    }
};

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值