数据结构与算法 - 单调栈 - 01 单调栈模板及模板题

314 阅读6分钟

简介

单调栈是一种特殊的栈,栈内的元素,会单调递增或者单调递减

常见的是用来在数组中找某元素左边或者右边第一个比它或者的数

模板题

题目地址

给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

输入格式

第一行包含整数 N,表示数列长度。

第二行包含 N 个整数,表示整数数列。

输出格式

共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。

数据范围

1≤N≤105

1≤数列中元素≤109

输入样例:

5
3 4 2 7 5

输出样例:

-1 3 -1 2 2

思路:

简单做法:

就是针对每个值, 遍历该值前边的值做比较, 复杂度是 n2

优化思路:

  1. 借助栈, 从位置0开始, 每当放入元素x时, 将栈中比x小的值都出栈, 因为当x入栈后,x比以前的元素更小, 而且位置更靠右, 所以以前的栈里大于x的值都已经不会再有可能用到了
  2. 当将所有小于x的元素都出栈后, 这时候栈顶元素就是左边第一个小于x的数

因为每次入栈x时, 都会将栈中所有大于x的数出栈, 所以这个栈是一个单调递增的一个栈, 我们称这种栈为单调栈

模拟:

3 4 2 7 5

操作将大于自身的出栈输出入栈
3入栈[]-1[3]
4入栈[3]3[3, 4]
2入栈[]-1[2]
7入栈[2]2[2, 7]
5入栈[2]2[2, 5]

代码:

#include <iostream>
using namespace std;

const int N = 100010;

int nums[N], stk[N], res[N], top = 0;

int main() {
    
    int n;
    cin >> n;
    
    for(int i = 0; i < n; i++) {
        cin >> nums[i];
    }
    
    for(int i = 0; i < n; i++) {
        int idx = i;
        int x = nums[idx];
        // 将大于自身的值全部出栈
        while(top > 0 && stk[top] <= x) {
            top--;
        }
        // 输出结果
        if (top == 0) {
            res[idx] = -1;
        } else {
            res[idx] = stk[top];
        }
        // 入栈
        stk[++top] = x;
    }
    
    for(int i = 0; i < n; i++) {
        cout << res[i] << " ";
    }
    
    cout << endl;
    
    return 0;
}

引申: 通用模板

  1. 找左边第一个比它小的数 (A C组合)
  2. 找左边第一个比它大的数 (A D组合)
  3. 找右边第一个比它小的数 (B C组合)
  4. 找右边第一个比它大的数 (B D组合)
func getNextGreaterArr(_ nums: [Int]) -> [Int] {
    var stack = [Int]()
    var res = Array(repeating: 0, count: nums.count)
    // A: 如果是找左边的值, 则 var indexes = 0..<nums.count
    // B: 如果是找右边的值, 则 var indexes = Array(0..<nums.count).reversed()
    var indexes = Array(0..<nums.count).reversed()
    
    for i in indexes {
        var x = nums[i]
        
        // 出栈逻辑
        // C: 如果是找第一个比当前元素小的值, 则大于等于本身的出栈, 则stk[top] >= x (找第一个小于等于自己的元素时, 用 > 取代 >= )
        // D: 如果是找第一个比当前元素大的值, 则小于等于本身的出栈, 则stk[top] <= x (找第一个大于等于自己的元素时, 用 < 取代 <=)
        while !stack.isEmpty && nums[stack.last!] <= x {
            stack.removeLast()
        }
        
        // 输出
        if stack.isEmpty {
        // -1 找不到目标值后的默认值
            res[i] = -1
        } else {
        // 如果要输出位置, 则res[i] = stack.last!, 如果要输出值, 则res[i] = nums[stack.last!]
            res[i] = nums[stack.last!]
        }
        
        // 入栈: 入栈的是位置, 不是值本身 (如果需要输出的是值,不是位置,也可以直接把值入栈)
        stack.append(i)
    }
    return res
}
  1. 根据找左边 和 找右边, 来决定遍历顺序, 左边从0到n, 右边从n-1到0
  2. 根据找大于 还是 小于 x的第一个元素, 来决定跟栈顶元素比较的符号, 小于是: 栈顶元素 >= x, 小于等于是: 栈顶元素 > x, 大于是: 栈顶元素 <= x, 大于等于是: 栈顶元素 < x

相关题目

下面10道题的核心是可以直接套模板的

496. 下一个更大元素 I

image.png

思路

看到找右边第一个比x大的元素, 就应该想到单调栈. 正好是我们的模板 B D

利用单调栈, 找到每个元素x右边第一个比x大的数, 结果是数组arr, 利用nums2生成一个map, map的key是值, value是位置, 那么遍历nums1, 从map中获取到对应的数字在nums2中的位置, 利用arr数组, 就可以找到对应值

代码

class Solution {
    func nextGreaterElement(_ nums1: [Int], _ nums2: [Int]) -> [Int] {
        // 模板: 找到每个元素`x`右边第一个比`x`大的数
        func getNextGreaterArr(_ nums: [Int]) -> [Int] {
            var stack = [Int]()
            var res = Array(repeating: 0, count: nums.count)
            var indexes = Array(0..<nums.count).reversed()

            for i in indexes {
                var x = nums[i]
                while !stack.isEmpty && stack.last! <= x {
                    stack.removeLast()
                }
                if stack.isEmpty {
                    res[i] = -1
                } else {
                    res[i] = stack.last!
                }
                stack.append(x)
            }
            return res
        }
    
        // 找到每个元素`x`右边第一个比`x`大的数
        var arr = getNextGreaterArr(nums2)
        
        // 利用`nums2`生成一个map, map的key是值, value是位置
        var map = [Int: Int]()
        for i in 0..<nums2.count {
            map[nums2[i]] = i
        }
        
        // 遍历`nums1`, 从`map`中获取到对应的数字在`nums2`中的位置, 利用`arr`数组, 就可以找到对应值
        var res = [Int]()
        for x in nums1 {
            res.append(arr[map[x]!])
        }

        return res
    }
}

503. 下一个更大元素 II

image.png

思路:

找下一个更大元素跟496题一样

但是这个题的数组是一个循环数组, 找循环数组的下一个更大元素, 可以模拟一下循环数组

i位置, 找下个更大的元素, 其实就是从i+1到n-10到i-1两个位置序列中找到第一个更大的元素

但如果把数组复制一份,拼接在自己的数组后面, 这样, 新数组中,0到i-1n到n+i-1是完全一样的

所以查找i位置的下个更大元素, 就可以转变成, 从i+1到n-1n到n+i-1两个的序列中找到第一个更大的元素, 那0到n-1就可以按照普通数组的方式来找了

代码:

class Solution {
    func nextGreaterElements(_ nums: [Int]) -> [Int] {
        // 模板: 找到每个元素`x`右边第一个比`x`大的数, 跟496使用的模板代码完全一致
        func getNextGreaterArr(_ nums: [Int]) -> [Int] {
            var stack = [Int]()
            var res = Array(repeating: 0, count: nums.count)
            var indexes = Array(0..<nums.count).reversed()

            for i in indexes {
                var x = nums[i]
                while !stack.isEmpty && stack.last! <= x {
                    stack.removeLast()
                }
                if stack.isEmpty {
                    res[i] = -1
                } else {
                    res[i] = stack.last!
                }
                stack.append(x)
            }
            return res
        }
    
        // 找到每个元素`x`右边第一个比`x`大的数, 传入的数组是拼接后的数组
        var arr = getNextGreaterArr(nums + nums)
        return Array(arr[0..<nums.count])
    }
}

739. 每日温度

image.png

思路:

实则就是找元素x右边第一个大于x的数所在的位置, 如果找不到, 输出0, 如果找到了, 输出(栈顶位置 - 当前位置)

代码

class Solution {
    func dailyTemperatures(_ temperatures: [Int]) -> [Int] {
            // 模板: 找到每个元素`x`右边第一个比`x`大的数
        func getNextGreaterArr(_ nums: [Int]) -> [Int] {
            var stack = [Int]()
            var res = Array(repeating: 0, count: nums.count)
            var indexes = Array(0..<nums.count).reversed()

            for i in indexes {
                while !stack.isEmpty && nums[stack.last!] <= nums[i] {
                    stack.removeLast()
                }
                if stack.isEmpty {
                    res[i] = 0
                } else {
                    res[i] = stack.last! - i
                }
                stack.append(i)
            }
            return res
        }
    
        // 找到每个元素`x`右边第一个比`x`大的数
        return getNextGreaterArr(temperatures)
    }
}

1019. 链表中的下一个更大节点

image.png

思路:

转换成数组, 就是找每个元素x右边第一个比x大的数, 找不到下一个更大节点的情况下, 要返回0而不是-1

代码:

class Solution {
    func nextLargerNodes(_ head: ListNode?) -> [Int] {
         // 模板: 找到每个元素`x`右边第一个比`x`大的数
        func getNextGreaterArr(_ nums: [Int]) -> [Int] {
            var stack = [Int]()
            var res = Array(repeating: 0, count: nums.count)
            var indexes = Array(0..<nums.count).reversed()

            for i in indexes {
                var x = nums[i]
                while !stack.isEmpty && stack.last! <= x {
                    stack.removeLast()
                }
                if stack.isEmpty {
                    res[i] = 0 // 注意输出的默认值
                } else {
                    res[i] = stack.last!
                }
                stack.append(x)
            }
            return res
        }
    
        var nums = [Int]()
        var head = head
        
        while head != nil {
            nums.append(head!.val)
            head = head?.next
        }
        return getNextGreaterArr(nums)
    }
}

1475. 商品折扣后的最终价格

image.png

思路:

实则就是找元素x右边第一个小于等于x的数

  1. 输出的结果的话, 如果找不到小于x的数, 输出x本身, 如果找到小于x的数y, 输出x - y
  2. 出栈的判断, 因为要找小于等于当前元素的, 所以严格大于自身才需要出栈

代码:

class Solution {
    func finalPrices(_ prices: [Int]) -> [Int] {
         // 模板: 找到每个元素`x`右边第一个比`x`小的数
        func getNextSmallerArr(_ nums: [Int]) -> [Int] {
            var stack = [Int]()
            var res = Array(repeating: 0, count: nums.count)
            var indexes = Array(0..<nums.count).reversed()

            for i in indexes {
                var x = nums[i]
                // 出栈判断
                while !stack.isEmpty && stack.last! > x {
                    stack.removeLast()
                }
                // 输出
                if stack.isEmpty {
                    res[i] = x
                } else {
                    res[i] = x - stack.last!
                }
                // 入栈
                stack.append(x)
            }
            return res
        }

        return getNextSmallerArr(prices)
    }
}

1944. 队列中可以看到的人数

image.png

思路:

  1. 实际上可以转换成, 找位置为i的元素x右边第一个大于x的元素, 假设我们找到了位置为jy元素, 因为这时候能看到y, 但是比y更高的,x肯定看不到了, 所以能看到的元素一定全部在ij之间,包括j
  2. 借助栈, x元素入栈时, 小于等于x的栈中元素, 因为x的存在, 别人也一定看不到, 所以可以直接出栈, 所以是一个单调递减的一个栈
  3. 每个出栈的元素z, 都是严格小于等于x的, 而且zx之间, 不会存在比z高的元素, 因为如果存在比z高, z会出栈, 一定不会在栈中, 所有需要出栈的元素都是x能看到的

代码:

class Solution {
    func canSeePersonsCount(_ heights: [Int]) -> [Int] {
    // 模板: 找元素`x`右边第一个大于`x`的元素
        func getNextGreaterArr(_ nums: [Int]) -> [Int] {
            var res = Array(repeating: 0, count: nums.count)
            var stack = [Int]()
            var indexes = Array(0..<nums.count).reversed()
            for i in indexes {
                var x = nums[i]
                // s是计算出栈的次数, 也就是能看到的人数
                var s = 0
                while !stack.isEmpty && stack.last! <= x {
                    stack.removeLast()
                    s += 1
                }
                // 如果栈不为空, 说明找到了右边第一个大于`x`的元素, 这个元素也是能看到的, 需要额外 + 1
                res[i] = s + (stack.isEmpty ? 0 : 1)
                stack.append(x)
            }
            return res
        }
        return getNextGreaterArr(heights)
    }
}

84. 柱状图中最大的矩形

image.png

思路

矩形的面积是宽 * 高, 我们挨个遍历所有柱子,来计算以该柱子的高为矩形的高的情况下,能获取到的最大矩形面积, 然后从遍历到的结果中选取最大值

要以柱子x的高为矩形的高的情况下, 计算能获取到的最大矩形面试, 高固定的情况, 需要计算最大的宽度, 而最大的宽度, 可以通过找到左边第一个小于他的柱子l和右边第一个小于他的柱子r, 那么最大矩形面积就是(r - 1 - l) * x的高度

找到左边第一个小于他的柱子l和右边第一个小于他的柱子r, 就可以套用我们上边的模板了

代码

class Solution {
    func largestRectangleArea(_ h: [Int]) -> Int {
        let n = h.count
        var stack = [Int]()
        
        // indexes: 遍历顺序, defaultValue: 找不到比自己小的柱子时的默认值, 左边是-1, 右边是n
        func getArray(_ indexes: [Int], _ defaultValue: Int) -> [Int] {
            stack = [Int]()
            var res = Array(repeating: defaultValue, count: n)
            
            for i in indexes {
               // 大于本身的出栈, 所以用 >= , h[stack.last!] >= h[i] 
                while !stack.isEmpty && h[stack.last!] >= h[i] {
                    stack.removeLast()
                }
                
                // 输出结果
                if !stack.isEmpty {
                    res[i] = stack.last!
                }
                
                // 入栈
                stack.append(i)
            }
            
            return res
        }
        
        // 遍历, 找到每个柱子, 对应的左边第一个小于他的柱子, 因为是左边, 所以顺序从前到后
        let lm = getArray(Array(0..<n), -1)
        // 遍历, 找到每个柱子, 对应的右边第一个小于他的柱子, 因为是右边, 所以顺序从后到前
        let rm = getArray(Array((0..<n).reversed()), n)
        
        var res = 0
        
        for i in 0..<n {
            res = max(res, (rm[i] - lm[i] - 1) * h[i])
        }
        
        return res
    }
}

85. 最大矩形

image.png

思路:

最大矩形肯定有最下边的一行,那么就可以转换为求哪一行是最大矩形的最下边一行

其实可以转换为每一行为底部的柱状图, 遍历每行为底部的柱状图求最大面积, 拿到最大值, 而获取柱状图的最大面试正好就是84题

转换代码:

   for i in 0..<n {
        for j in 0..<m {
            if(i == 0) {
                h[i][j] = b[i][j].wholeNumberValue!
            }else{
                h[i][j] = b[i][j] == "1" ? (h[i-1][j] + 1) : 0
            }
        }
    }

原矩阵

image.png

转换后的柱状图, 遍历每行的柱状图的求最大面积

1 0 1 0 0

2 0 2 1 1

3 1 3 2 2

4 0 0 3 0

代码

class Solution {
    // 这段代码与84题完全相同
     func largestRectangleArea(_ h: [Int]) -> Int {
        let n = h.count
        func getArray(_ indexes: [Int], _ defaultValue: Int) -> [Int] {
            var stack = [Int]()
            var res = Array(repeating: defaultValue, count: n)
            for i in indexes {
                while !stack.isEmpty && h[stack.last!] >= h[i] {
                    stack.removeLast()
                }
                if !stack.isEmpty {
                    res[i] = stack.last!
                }
                stack.append(i)
            }
            return res
        }
        let lm = getArray(Array(0..<n), -1)
        let rm = getArray(Array((0..<n).reversed()), n)
        var res = 0
        for i in 0..<n {
            res = max(res, (rm[i] - lm[i] - 1) * h[i])
        }
        return res
    }

    func maximalRectangle(_ b: [[Character]]) -> Int {
        // 边界判空
        if(b.isEmpty || b.first!.isEmpty) {
            return 0
        }
        
        let n = b.count, m = b[0].count
        var h = Array(repeating: Array(repeating: 0, count: m), count: n)
        
        // 将矩形转换为 每行为柱状图底部的n个柱状图
        for i in 0..<n {
            for j in 0..<m {
                if(i == 0) {
                    h[i][j] = b[i][j].wholeNumberValue!
                }else{
                    h[i][j] = b[i][j] == "1" ? (h[i-1][j] + 1) : 0
                }
            }
        }
        
        // 比较每个柱状图的最大面积得出结果
        var res = 0;
        for i in 0..<n {
            res = max(res, largestRectangleArea(h[i]))
        }
        return res;
    }
}

1793. 好子数组的最大分数

image.png

思路:

这个好子数组的最大分数, 其实可以转换成求柱状图的最大面积, 也就是84题, 只是要求最大面积中包含k柱子的部分或全部面积

要以柱子x的高为矩形的高的情况下, 计算能获取到的最大矩形面试, 高固定的情况, 需要计算最大的宽度, 而最大的宽度, 可以通过找到左边第一个小于他的柱子l和右边第一个小于他的柱子r, 那么最大矩形面积就是(r - 1 - l) * x的高度, 但是k必须处于区间[l+1...r-1]中, 算出来的面积才有意义

代码:

class Solution {
    func maximumScore(_ h: [Int], _ k: Int) -> Int {
        let n = h.count
        var stack = [Int]()
        
        // indexes: 遍历顺序, defaultValue: 找不到比自己小的柱子时的默认值, 左边是-1, 右边是n
        func getArray(_ indexes: [Int], _ defaultValue: Int) -> [Int] {
            stack = [Int]()
            var res = Array(repeating: defaultValue, count: n)
            
            for i in indexes {
               // 大于本身的出栈, 所以用 >= , h[stack.last!] >= h[i] 
                while !stack.isEmpty && h[stack.last!] >= h[i] {
                    stack.removeLast()
                }
                
                // 输出结果
                if !stack.isEmpty {
                    res[i] = stack.last!
                }
                
                // 入栈
                stack.append(i)
            }
            return res
        }
        
        // 遍历, 找到每个柱子, 对应的左边第一个小于他的柱子, 因为是左边, 所以顺序从前到后
        let lm = getArray(Array(0..<n), -1)
        // 遍历, 找到每个柱子, 对应的右边第一个小于他的柱子, 因为是右边, 所以顺序从后到前
        let rm = getArray(Array((0..<n).reversed()), n)
        

        var res = 0
        for i in 0..<n {
            // 只有这里跟84有区别, k必须处于[lm[i]+1...rm[i]-1]中才有意义
            if((lm[i]+1...rm[i]-1).contains(k)) {
                res = max(res, (rm[i] - lm[i] - 1) * h[i])
            }
        }
        
        return res
    }
}

1856. 子数组最小乘积的最大值

image.png

思路:

这个子数组的最小乘积的最大值, 其实可以用求柱状图的最大面积的思路来考虑, 也就是84题

结果 = (子数组的最小值) * (子数组的和)

确定以元素x的为子数组中最小值的情况下, 要获取结果的最大值, 需要和更大的子数组, 因为所有元素都是正数,所以计算和最大的子数组, 就是计算最大的宽度, 而最大的宽度, 可以通过找到左边第一个小于他的柱子l和右边第一个小于他的柱子r, 那么结果就是([l+1...r-1]范围的和) * x

子数组的和可以用前缀和来优化

代码:

class Solution {
    func maxSumMinProduct(_ h: [Int]) -> Int {
         let n = h.count
        var stack = [Int]()
        
        // indexes: 遍历顺序, defaultValue: 找不到比自己小的柱子时的默认值, 左边是-1, 右边是n
        func getArray(_ indexes: [Int], _ defaultValue: Int) -> [Int] {
            stack = [Int]()
            var res = Array(repeating: defaultValue, count: n)
            
            for i in indexes {
               // 大于本身的出栈, 所以用 >= , h[stack.last!] >= h[i] 
                while !stack.isEmpty && h[stack.last!] >= h[i] {
                    stack.removeLast()
                }
                
                // 输出结果
                if !stack.isEmpty {
                    res[i] = stack.last!
                }
                
                // 入栈
                stack.append(i)
            }
            return res
        }
        
        // 遍历, 找到每个柱子, 对应的左边第一个小于他的柱子, 因为是左边, 所以顺序从前到后
        let lm = getArray(Array(0..<n), -1)
        // 遍历, 找到每个柱子, 对应的右边第一个小于他的柱子, 因为是右边, 所以顺序从后到前
        let rm = getArray(Array((0..<n).reversed()), n)
        

        // 前缀和
        var s = Array(repeating: 0, count: n + 1)
        for i in 1...n {
            s[i] = s[i - 1] + h[i - 1]
        }
        
        var res = 0
        for i in 0..<n {
            // 只有这里跟84有区别, 结果是 子数组的合 * 子数组中的最小值
            res = max(res, (s[rm[i]] - s[lm[i] + 1]) * h[i])
        }
        
        // 结果需要余1000000007
        return res % (1000000007)
    }
}