简介
单调栈是一种特殊的栈,栈内的元素,会单调递增或者单调递减
常见的是用来在数组中找某元素左边或者右边第一个比它大或者小的数
模板题
给定一个长度为 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
优化思路:
- 借助栈, 从位置0开始, 每当放入元素
x时, 将栈中比x小的值都出栈, 因为当x入栈后,x比以前的元素更小, 而且位置更靠右, 所以以前的栈里大于x的值都已经不会再有可能用到了 - 当将所有小于
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;
}
引申: 通用模板
- 找左边第一个比它小的数 (
A C组合) - 找左边第一个比它大的数 (
A D组合) - 找右边第一个比它小的数 (
B C组合) - 找右边第一个比它大的数 (
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
}
- 根据找左边 和 找右边, 来决定遍历顺序, 左边从0到n, 右边从n-1到0
- 根据找大于 还是 小于
x的第一个元素, 来决定跟栈顶元素比较的符号, 小于是: 栈顶元素 >= x, 小于等于是: 栈顶元素 > x, 大于是: 栈顶元素 <= x, 大于等于是: 栈顶元素 < x
相关题目
下面10道题的核心是可以直接套模板的
496. 下一个更大元素 I
思路
看到找右边第一个比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
思路:
找下一个更大元素跟496题一样
但是这个题的数组是一个循环数组, 找循环数组的下一个更大元素, 可以模拟一下循环数组
在i位置, 找下个更大的元素, 其实就是从i+1到n-1和0到i-1两个位置序列中找到第一个更大的元素
但如果把数组复制一份,拼接在自己的数组后面, 这样, 新数组中,0到i-1和n到n+i-1是完全一样的
所以查找i位置的下个更大元素, 就可以转变成, 从i+1到n-1和n到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. 每日温度
思路:
实则就是找元素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. 链表中的下一个更大节点
思路:
转换成数组, 就是找每个元素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. 商品折扣后的最终价格
思路:
实则就是找元素x右边第一个小于等于x的数
- 输出的结果的话, 如果找不到小于
x的数, 输出x本身, 如果找到小于x的数y, 输出x - y - 出栈的判断, 因为要找小于等于当前元素的, 所以严格大于自身才需要出栈
代码:
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. 队列中可以看到的人数
思路:
- 实际上可以转换成, 找位置为
i的元素x右边第一个大于x的元素, 假设我们找到了位置为j的y元素, 因为这时候能看到y, 但是比y更高的,x肯定看不到了, 所以能看到的元素一定全部在i到j之间,包括j - 借助栈,
x元素入栈时, 小于等于x的栈中元素, 因为x的存在, 别人也一定看不到, 所以可以直接出栈, 所以是一个单调递减的一个栈 - 每个出栈的元素
z, 都是严格小于等于x的, 而且z和x之间, 不会存在比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. 柱状图中最大的矩形
思路
矩形的面积是宽 * 高, 我们挨个遍历所有柱子,来计算以该柱子的高为矩形的高的情况下,能获取到的最大矩形面积, 然后从遍历到的结果中选取最大值
要以柱子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. 最大矩形
思路:
最大矩形肯定有最下边的一行,那么就可以转换为求哪一行是最大矩形的最下边一行
其实可以转换为每一行为底部的柱状图, 遍历每行为底部的柱状图求最大面积, 拿到最大值, 而获取柱状图的最大面试正好就是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
}
}
}
原矩阵
转换后的柱状图, 遍历每行的柱状图的求最大面积
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. 好子数组的最大分数
思路:
这个好子数组的最大分数, 其实可以转换成求柱状图的最大面积, 也就是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. 子数组最小乘积的最大值
思路:
这个子数组的最小乘积的最大值, 其实可以用求柱状图的最大面积的思路来考虑, 也就是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)
}
}