单调栈
单调栈
单调(递增/递减)栈是在栈的先进后出基础之上额外添加一个特性:从栈顶到栈底的元素是严格递增/递减
例子:
以单调递增栈为例,对于一组数:3,4,2,6,4,5,2,3. 让它们从左到右依次入栈,具体过程如下:
栈基础操作
- 初始化栈
// 初始化栈,数据类型为 Integer
Stack<Integer> s = new Stack<Integer>();
- 入栈 出栈
// 入栈
s.push(1);
// 出栈
s.pop();
// 获取栈顶元素
s.peek()
- 栈大小
// 栈大小
s.size();
// 是否为空
s.isEmpty();
单调栈场景
单调栈就是解决那些 需要单调递增 或递减的序列, 如下一个更大元素
单调栈模板
正序遍历维护单调递增栈
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
// 存放答案的数组
int[] ans = new int[n];
Arrays.fill(ans,-1);
// 存放的是下标
Stack<Integer> stack = new Stack<Integer>();
for(int i=0;i<n; i++){
// 找栈顶元素的下一个更大元素:即值比栈顶元素大
while(!stack.empty() && nums[i]>nums[stack.peek()]){
// 栈顶元素
int top = stack.pop();
// 当前元素是栈顶元素的下一个最大元素
ans[top] = nums[i];
}
// 比栈顶元素小直接入栈
stack.push(i);
}
return ans;
}
倒序遍历维护单调递增栈
例题
42. 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
提示:
n == height.length
0 <= n <= 3 * 104
0 <= height[i] <= 105
思路:
有坑才能接住雨水,所谓的坑其实就是有左右两个边界,那我们只要找到每个坑的边界,就可以通过这个边界的距离和高度得到这个坑里可以接到的雨水。
可以通过单调递增栈来实现:当栈顶元素不为空且当前元素大于栈顶元素时,相当于我们找到了一个坑:当前元素就是坑的右边界,被弹出的元素就是坑内的高度,新的栈顶就是坑的左边界,这样就可以算出来这个坑可以接的雨水数量。
本质上还是求下一个最大元素,只不过找到下一个最大元素后要转换过来找到的是一个坑,栈顶为坑底,并且栈顶弹出后的栈顶为坑的左边界,找到的下一个更大元素为坑的右边界。求面积的时候要减去坑底的高度
class Solution {
public int trap(int[] height) {
int n = height.length;
Stack<Integer> stack = new Stack<Integer>();//存索引
int ans = 0;
for(int i =0;i<n;i++){
//当前元素比栈顶元素大,相当于找到一个坑,当前元素就是坑的右边界
while(!stack.empty()&&height[i]>height[stack.peek()]){
//出栈的是坑里的元素的高度的索引
int top = stack.pop(); //出栈的栈顶元素的索引
if(stack.empty()){
break;
}
//找到两边边界的距离,新的栈顶就是坑的左边界
int distance = i-stack.peek()-1;
//找到两边低的那个减去里面的
int bounded_height = Math.min(height[i], height[stack.peek()])-height[top];
ans += distance*bounded_height;
}
//入栈
stack.push(i);
}
return ans;
}
}
496. 下一个更大元素 I
给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。
请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
示例 1:
输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:
对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。
对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。
对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
示例 2:
输入: nums1 = [2,4], nums2 = [1,2,3,4].
输出: [3,-1]
解释:
对于 num1 中的数字 2 ,第二个数组中的下一个较大数字是 3 。
对于 num1 中的数字 4 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
提示:
1 <= nums1.length <= nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 104
nums1和nums2中所有整数 互不相同
nums1 中的所有整数同样出现在 nums2 中
进阶:你可以设计一个时间复杂度为 O(nums1.length + nums2.length) 的解决方案吗?
思路:
因为是要找下一个更大元素,所以可以用一个单调递增栈,如果栈不为空,且当前元素比栈顶元素大相当于我们就找到了栈顶元素的下一个更大元素。
由于这个题目是找nums2的子集nums1的下一个更大元素,所以我们可以把nums2的下一个最大元素都找到,把对应关系放到map里。然后从map里找到nums1里对应的值。
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int n1 = nums1.length;
int n2 = nums2.length;
Stack<Integer> stack = new Stack<Integer>();
Map<Integer,Integer> map = new HashMap<Integer,Integer>();//[值,对应最大值]
int[] ans = new int[n2];
int[] ret = new int[n1];
//用单调栈找到nums2的每个元素的下一个最大元素
for(int i=0;i<n2;i++){
while(!stack.empty() && nums2[i]>nums2[stack.peek()]){
int top = stack.pop();//栈顶元素出栈
ans[top] = nums2[i]; //栈顶元素的下一个更大元素是 当前元素
}
stack.push(i);
}
//把nums2的值和对应的下一个元素存到map中
for(int i=0;i<n2;i++){
if(ans[i]!=0){
map.put(nums2[i],ans[i]);
}
else{
map.put(nums2[i],-1);
}
}
//根据map得到nums1的下一个最大元素的值
for(int i=0;i<n1;i++){
ret[i] = map.get(nums1[i]);
}
return ret;
}
}
503. 下一个更大元素 II
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
示例 1:
输入: [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数;
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
注意: 输入数组的长度不会超过 10000。
思路:
由于是循环数组,我们要找下一个更大元素 就应该将数组长度翻倍,我们不需要构造新数组,只需要在处理时对下标取模即可。
class Solution {
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
Arrays.fill(ans,-1);
Stack<Integer> stack = new Stack<Integer>();
for(int i=0;i<2*n-1; i++){
int cur = i%n;
while(!stack.empty() && nums[cur]>nums[stack.peek()]){
// 栈顶元素
int top = stack.pop();
// 当前元素是栈顶元素的下一个最大元素
ans[top] = nums[cur];
}
stack.push(cur);
}
return ans;
}
}
739. 每日温度
请根据每日气温列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
思路:
观测更高的气温其实就是比找到下一个更大元素,至少要等到的天数就是找到的下一个更大元素的坐标减去当前元素的坐标
也就是说,这道题目就是给你一个值,让你找到右边第一个比它大的数,它们两则的下标差就是输出结果。
遍历整个数组,如果栈不空,且当前数字大于栈顶元素,那么如果直接入栈的话就不是 递增栈 ,所以需要取出栈顶元素,由于当前数字大于栈顶元素的数字,而且一定是第一个大于栈顶元素的数,直接求出下标差就是二者的距离。
class Solution {
public int[] dailyTemperatures(int[] T) {
int n = T.length;
int[] ans = new int[n];
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < n; i++) {
while (!stack.empty() && T[i]>T[stack.peek()]){
// 栈顶元素
int top = stack.pop();
// 两个坐标的差就是栈顶元素的需要等待的天数
ans[top] = i-top;
}
stack.push(i);
}
return ans;
}
}
901. 股票价格跨度
901. 股票价格跨度
编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。
今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。
例如,如果未来7天股票的价格是 [100, 80, 60, 70, 60, 75, 85],那么股票跨度将是 [1, 1, 1, 2, 1, 4, 6]。
示例:
输入:[“StockSpanner”,“next”,“next”,“next”,“next”,“next”,“next”,“next”], [[],[100],[80],[60],[70],[60],[75],[85]]
输出:[null,1,1,1,2,1,4,6]
解释:
首先,初始化 S = StockSpanner(),然后:
S.next(100) 被调用并返回 1,
S.next(80) 被调用并返回 1,
S.next(60) 被调用并返回 1,
S.next(70) 被调用并返回 2,
S.next(60) 被调用并返回 1,
S.next(75) 被调用并返回 4,
S.next(85) 被调用并返回 6。
注意 (例如) S.next(75) 返回 4,因为截至今天的最后 4 个价格
(包括今天的价格 75) 小于或等于今天的价格。
提示:
调用 StockSpanner.next(int price) 时,将有 1 <= price <= 10^5。
每个测试用例最多可以调用 10000 次 StockSpanner.next。
在所有测试用例中,最多调用 150000 次 StockSpanner.next。
此问题的总时间限制减少了 50%。
思路:
股票价格的跨度 就是从后往前遍历 找到比当前元素大的第一个元素的坐标, 两个坐标相减
但是这个题目给我们的不是数组,而是通过初始化StockSpanner类,然后调用其next()方法得到结果,所以我们需要在初始化StockSpanner的时候初始化prices和weights两个栈, 通过weights 来得到跨度
class StockSpanner {
Stack<Integer> prices, weights;
public StockSpanner() {
prices = new Stack();
weights = new Stack();
}
public int next(int price) {
int w = 1;
while (!prices.isEmpty() && prices.peek() <= price) {
prices.pop();
w += weights.pop();
}
prices.push(price);
weights.push(w);
return w;
}
}
84. 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例:
输入: [2,1,5,6,2,3]
输出: 10
思路:
对每个柱子来说,找到这个柱子左边和右边第一个比自己小的元素,这样就相当于找到了以自己的高度为高的最大矩形面积
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int ans = 0;
Stack<Integer> stack = new Stack<>();
int[] left = new int[n];
int[] right = new int[n];
Arrays.fill(left,-1);
Arrays.fill(right,n);
// 从右往左遍历,找到每个柱子左边第一个比自己小的元素
for (int i = n-1; i >=0; i--) {
while(!stack.empty() && heights[i]<heights[stack.peek()]){
int top = stack.pop();
left[top] = i;
}
stack.push(i);
}
stack.clear();
// 从左到右遍历,找到每个柱子右边离自己最近的比自己小的元素
for (int i = 0; i < n; i++) {
while(!stack.empty() && heights[i] < heights[stack.peek()]){
int top = stack.pop();
right[top] = i;
}
stack.push(i);
}
for (int i = 0; i < n; i++) {
int w = right[i]-left[i]-1;
ans = Math.max(ans,w*heights[i]);
}
return ans;
}
}
85. 最大矩形
给定一个仅包含 0 和 1 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
示例 1:
输入:matrix = [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”]
,[“1”,“0”,“0”,“1”,“0”]]
输出:6
解释:最大矩形如上图所示。
示例 2:
输入:matrix = []
输出:0
示例 3:
输入:matrix = [[“0”]]
输出:0
示例 4:
输入:matrix = [[“1”]]
输出:1
示例 5:
输入:matrix = [[“0”,“0”]]
输出:0
提示:
rows == matrix.length
cols == matrix[0].length
0 <= row, cols <= 200
matrix[i][j] 为 ‘0’ 或 ‘1’
把这道题稍微一转换 就可以用84题
我们可以以每一行为x坐标,这样就每次调用84得到以当前行为x轴的最大矩形面积了
class Solution {
public int maximalRectangle(char[][] matrix) {
if (matrix.length == 0) {
return 0;
}
int[] heights = new int[matrix[0].length];
int maxArea = 0;
// 以每一行为x轴
for (int row = 0; row < matrix.length; row++) {
// 遍历每一列,更新高度
for (int col = 0; col < matrix[0].length; col++) {
if (matrix[row][col] == '1') {
heights[col] += 1;
} else {
heights[col] = 0;
}
}
// 求以当前row为x轴的最大矩形面积
maxArea = Math.max(maxArea, largestRectangleArea(heights));
}
return maxArea;
}
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int[] left = new int[n];
int[] right = new int[n];
Arrays.fill(left,-1);
Arrays.fill(right,n);
int ans = 0;
Stack<Integer> stack = new Stack<>();
// 从右往左遍历 找到每个柱子左边第一个比自己小的元素
for (int i = n - 1; i >= 0; i--) {
while (!stack.isEmpty() && heights[i] < heights[stack.peek()]) {
int top = stack.pop();
left[top] = i;
}
stack.push(i);
}
stack.clear();
// 从左往右遍历 找到每个柱子右边第一个比自己小的元素
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && heights[i] < heights[stack.peek()]) {
int top = stack.pop();
right[top] = i;
}
stack.push(i);
}
// 求出最大面积
for (int i = 0; i < n; i++) {
int w = right[i] - left[i] - 1;
ans = Math.max(ans, w * heights[i]);
}
return ans;
}
}
239. 滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
示例 3:
输入:nums = [1,-1], k = 1
输出:[1,-1]
示例 4:
输入:nums = [9,11], k = 2
输出:[11]
示例 5:
输入:nums = [4,-2], k = 2
输出:[4]
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
1 <= k <= nums.length
思路:用一个双向队列维护当前滑动窗口内的元素单调递增(元素从左到右依次减小),这样每个滑动窗口的最大值就是当前队列中最左边的元素
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
// 用双向队列 维护单调递增
Deque<Integer> deque = new LinkedList<>();
// 得到当前滑动窗口的 单调递增队列
for (int i = 0; i < k; i++) {
// 单调递增栈
while(!deque.isEmpty() && nums[i]>=nums[deque.peekLast()]){
deque.pollLast();
}
deque.offerLast(i);
}
int[] ans = new int[n-k+1];
ans[0] = nums[deque.peekFirst()];
for (int i = k; i < n; i++) {
// 维护当前窗口的 单调递增队列
while(!deque.isEmpty() && nums[i]>=nums[deque.peekLast()]){
deque.pollLast();
}
deque.offerLast(i);
// 超出当前滑动窗口的范围
while(deque.peekFirst() <= i-k){
deque.pollFirst();
}
ans[i-k+1] = nums[deque.peekFirst()];
}
return ans;
}
}
栈题目
基本计算器( LeetCode 224 )
给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值
题解:
- 本质上都是左表达式 运算符 右表达式
- 运算符有+有-,我们用sign=1表示+,sign=-1表示-,所以表达式为:
左表达式 + sign*右表达式 - 如遇(,则将左表达式 和 运算符放入栈,并还原res和sign为默认值
- 如遇),则当前res为右表达式,从栈中取出左表达式和运算符进行计算
class Solution {
public int calculate(String s) {
// 栈存储括号里的计算
Stack<Integer> stack = new Stack<Integer>();
// 最后结果
int res = 0;
// 表示正负
int sign = 1;
int length = s.length();
for (int i = 0; i < length; i++) {
Character c = s.charAt(i);
if (Character.isDigit(c)){
// 找到全部数字
int curNum = c-'0';
while (i+1<length && Character.isDigit(s.charAt(i+1))){
i++;
curNum = curNum*10 + s.charAt(i)-'0';
}
res = res+sign*curNum;
}else if(c=='+'){
sign = 1;
}else if (c=='-'){
sign = -1;
}else if (c=='('){
// 把左表达式和运算符入栈,为和括号里的结果运算做准备
// 1、左表达式
stack.push(res);
res = 0;
// 2、运算符
stack.push(sign);
sign=1;
}else if (c==')'){
// 因为遇到(时已将res置0,所以当前res为括号里的值即 右表达式
// 1、将运算符出栈
int fSign = stack.peek();
stack.pop();
// 2、将左表达式出栈
int fRes = stack.peek();
stack.pop();
// 3、计算结果
res = fRes+fSign*res;
}
}
return res;
}
}
最小栈( LeetCode 155 )
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
MinStack() 初始化堆栈对象。
void push(int val) 将元素val推入堆栈。
void pop() 删除堆栈顶部的元素。
int top() 获取堆栈顶部的元素。
int getMin() 获取堆栈中的最小元素。
示例 1:
输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
提示:
-231 <= val <= 231 - 1
pop、top 和 getMin 操作总是在 非空栈 上调用
push, pop, top, and getMin最多被调用 3 * 104 次
Related Topics
栈
设计
https://www.algomooc.com/608.html
主要思路是除普通栈外再弄一个最小栈,栈顶元素永远存放当前最小值
class MinStack {
Stack<Integer> stack;
Stack<Integer> minStack;
public MinStack() {
stack = new Stack<Integer>();
minStack = new Stack<Integer>();
}
public void push(int val) {
stack.push(val);
if (minStack.isEmpty()){
minStack.push(val);
}else{
int topValue = minStack.peek();
if (val<=topValue){
minStack.push(val);
}
}
}
public void pop() {
int topValue = stack.peek();
int minTopValue = minStack.peek();
stack.pop();
// 判断普通栈中刚刚移除的栈顶元素值是否和此时辅助栈中的栈顶元素相同,如果是则将辅助栈中的栈顶元素移除,否则不执行操作,这样的目的是为了让辅助栈中的栈顶元素始终是普通栈中的最小值。
if (topValue==minTopValue){
minStack.pop();
}
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
验证栈序列
给定 pushed 和 popped 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。
示例 1:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
示例 2:
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。
思路:关键是用index索引poped,用来判断当前栈顶元素是否出栈
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
//1、设置一个索引 index 表示 popped 数组中元素的下标,判断该索引指向的元素能否正常的出栈
//2、遍历 pushed 数组中的每个元素,在遍历 pushed 数组时,把当前遍历的元素加入到栈中
//3、加入完之后,不断的执行以下的判断
//3.1、栈中是否有元素
//3.2、栈顶元素是否和 popped 当前下标的元素相同
//4、如果同时满足这两个条件,说明这个元素可以满足要求,即可以在最初空栈上进行推入 push 和弹出 pop 操作
//5、遍历完 pushed 数组中的每个元素之后,如果发现栈不为空,那么说明出栈序列不合法,返回 false
//6、遍历完 pushed 数组中的每个元素之后,如果发现栈为空,那么说明出栈序列合法,返回 true
// popped下标
int index = 0;
Stack<Integer> stack = new Stack<Integer>();
for (int i=0;i<pushed.length;i++){
stack.push(pushed[i]);
// 栈顶元素可弹出
while (!stack.isEmpty()&&stack.peek()==popped[index]){
stack.pop();
index++;
}
}
return stack.isEmpty();
}
}