单调队列和单调栈相关题目总结

单调队列、单调栈

Posted by WenlSun" on June 3, 2020

单调栈

单调栈常用来解决 查找每个数左侧第一个比它小(大、小于等于、大于等于)的数的问题。

柱状图中最大的矩形(困难)

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。LeetCode84

思路:枚举所有柱形的上边界,作为整个矩形的上边界,然后去求左右边界。
1.找出左边离它最近的,比它小的柱形 left
2.找出右边离它最近的,比它小的柱形 right

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int largestRectangleArea(vector<int>& heights) {
    int n = heights.size();
    vector<int> left(n), right(n);
    stack<int> stk;
    // 从左向右遍历,维护一个单调递减的栈
    for (int i = 0; i < n; i++) {
        while (stk.size() && heights[stk.top()] >= heights[i]) stk.pop();
        if (stk.empty())
            left[i] = -1;  // 注意这儿相当于负无穷
        else
            left[i] = stk.top();
        stk.push(i);
    }
    while (stk.size()) stk.pop();  // 需要将栈弹空
    // 向右向左遍历,维护一个单调递减的栈
    for (int i = n - 1; i >= 0; i--) {
        while (stk.size() && heights[stk.top()] >= heights[i]) stk.pop();
        if (stk.empty())
            right[i] = n;
        else
            right[i] = stk.top();
        stk.push(i);
    }
    int res = 0;
    // 遍历一次 更新答案
    for (int i = 0; i < n; i++)
        res = max(res, heights[i] * (right[i] - left[i] - 1));
    return res;
}

接雨水(困难)

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

思路:维护一个单调递减的栈

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int trap(vector<int>& height) {
    int res = 0;
    stack<int> stk;
    for (int i = 0; i < height.size(); i++) {
        int last = 0;  // 记录之前的高度
        // 维护一个单调递减栈
        while (stk.size() && height[stk.top()] <= height[i]) {
            int t = stk.top();
            stk.pop();
            // 当前层的雨水
            res += (height[t] - last) * (i - t - 1);
            last = height[t];  // 之前的高度更新为当前高度
        }
        // 右边墙低于左边墙的情况
        if (stk.size()) res += (height[i] - last) * (i - stk.top() - 1);
        stk.push(i);
    }
    return res;
}

每日温度(中等)

根据每日 气温 列表,请重新生成一个列表,对应位置的输出是需要再等待多久温度才会升高超过该日的天数。如果之后都不会升高,请在该位置用 0 来代替。每日温度

参考代码

1
2
3
4
5
6
7
8
9
10
11
vector<int> dailyTemperatures(vector<int>& T) {
    vector<int> res(T.size());
    stack<int> stk;
    for (int i = T.size() - 1; i >= 0; i--) {
        // 维护一个单调递减栈
        while (!stk.empty() && T[stk.top()] <= T[i]) stk.pop();
        res[i] = stk.empty() ? 0 : stk.top() - i;
        stk.push(i);
    }
    return res;
}

移掉k位数字(中等)

给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小。移掉K位数字

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
   public:
    /**
     * 单调栈
     * 利用栈存储当前迭代数字之前的数字,对于每一个数字,和栈顶元素比较,即该数字的左邻居
     * 如果小于栈顶元素,则出栈。
     * 从左向右依次扫描,如果前一个位置的数字比当前数字大,则把前一个数字删除,如果是递增的话,从最后面删除
     * 时间复杂度:O(n)
     * 空间复杂度:O(n)
     */
    string removeKdigits(string num, int k) {
        stack<char> stk;
        for (auto c : num) {
            while (k && stk.size() && stk.top() > c) { // 单调递增栈
                stk.pop();
                k--;
            }
            stk.push(c);
        }
        while(k > 0) stk.pop(), k--; // 如果原本就是递增的,则在末尾删除
        string res;
        while (stk.size()) {
            res += stk.top();
            stk.pop();
        }
        reverse(res.begin(), res.end());
        int idx = 0;
        while (res[idx] == '0') idx++;
        res = res.substr(idx);
        return res == "" ? "0" : res;
    }
};

去除重复字母/不同字符的最小子序列(困难)

给你一个仅包含小写字母的字符串,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证返回结果的字典序最小(要求不能打乱其他字符的相对位置)。去除重复字母,不同字符的最小子序列

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
   public:
    /**
     * 单调栈
     * 时间复杂度:O(n)
     * 空间复杂度:O(n)
     */
    string removeDuplicateLetters(string s) {
        if (s.empty()) return s;
        unordered_map<char, int> pos; // 用来存储每个字符最后出现的位置
        unordered_set<char> hash; // 用来记录栈中字符
        stack<int> stk; // 单调递增栈
        int n = s.size();
        for (int i = 0; i < n; i++) pos[s[i]] = i;
        for (int i = 0; i < n; i++) {
            auto c = s[i];
            if (!hash.count(c)) { // 如果栈中不存在当前字符
                // 维护单调递增栈,并保证在之后还有栈顶字母
                while (stk.size() && stk.top() > c && pos[stk.top()] > i) {
                    hash.erase(stk.top());
                    stk.pop();
                }
                stk.push(c);
                hash.insert(c);
            }
        }
        string res;
        while (stk.size()) {
            res += stk.top();
            stk.pop();
        }
        reverse(res.begin(), res.end());
        return res;
    }
};

单调队列

常用求解 滑动窗口中的最值

滑动窗口的最大值(困难)

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。LeetCode239

思路:维护一个单调队列

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
    vector<int> res;
    deque<int> q;
    for (int i = 0; i < nums.size(); i++){
        // 判断队头是否需要出队
        if (q.size() && i -k + 1 > q.front()) q.pop_front();
        // 维护一个单调队列
        while (q.size() && nums[q.back()] < nums[i]) q.pop_back();
        q.push_back(i);
        // 判断是否需要添加进结果数组中
        if (i >= k - 1) res.push_back(nums[q.front()]);
    }
    return res;
}

环形子数组的最大和(中等)

给定一个由整数数组 A 表示的环形数组 C,求 C 的非空子数组的最大可能和。LeetCode918

思路:将环形数组展开成两倍的普通数组,然后求前缀和,再维护一个单调递减的队列,转化为给定区间求最小的问题

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int maxSubarraySumCircular(vector<int>& A) {
    int n = A.size();
    // 将环形链表展成两倍的数组
    for (int i = 0; i < n; i++) A.push_back(A[i]);
    vector<int> sum(n * 2 + 1);
    // 求前缀和
    for (int i = 1; i <= 2 * n; i++) sum[i] = sum[i - 1] + A[i - 1];
    int res = INT_MIN;
    deque<int> q;
    q.push_back(0);  //  前缀和为0的情况
    for (int i = 1; i <= 2 * n; i++) {
        // i -n > q.front()) 如何理解??
        if (q.size() && i - n > q.front()) q.pop_front();
        // 更新答案
        if (q.size()) res = max(res, sum[i] - sum[q.front()]);
        // 维护一个单调递减队列
        while (q.size() && sum[q.back()] >= sum[i]) q.pop_back();
        q.push_back(i);
    }
    return res;
}