1.单调栈可以解决的问题:
我们常常遇到要在O(N)的时间复杂度内求出当前对象 nums[i] 左边或者右边首个比自己大或者小的情况
这种问题通常称为Next Greater问题
就相当于你是一个人,往两边看首先被哪个高个子挡住,这个高个子满足两个条件:
1.身高比你高;2.在某一方向中比你高的人中距离你最近
这种条件可能比较明显,739. 每日温度 496. 下一个更大元素 I 这类型要你直接求下一个更大更小的元素是什么
也有可能是比较隐秘的,抽象成波及范围的,这属于单调栈中的困难题了 42. 接雨水 6119. 元素值大于变化阈值的子数组
求某个元素的影响范围,通常都是找到两边首个比自己小的或大的 left 与 right,然后这个[left,right]通常就是影响区间的
例如[left,right]可以接住高度为max-nums[i]的雨水,可以组成某某区间等…
2.单调栈的两种写法:
以739. 每日温度 为例进行说明,一般来说有两种写法:
1.Next Greater元素在栈中的写法(25ms/51.1MB)
举个🌰:现在要寻找的nums[i]左边首个严格小于nums[i]的元素,st = [1, 2, 3, 6]
若nums[i]=7,直接取栈顶的6作为左边首个严格小于nums[i]的元素
若nums[i]=3,要将栈顶中>=nums[i]的元素全部弹出,直至栈顶严格小于nums[i] 此时st = [1, 2],那么2就是所求
要点: 如果找nums[i] 右边的元素,那么从右边遍历起以便于 Next Greater元素先入栈;
反之,如果找nums[i] 左边的元素,那么从左边遍历起以便于 Next Greater元素先入栈;
当前遍历到的nums[i]就是基准元素,如果找右边首个大于nums[i]的就要一直保持nums[i]是最左边的且是最小的,不符合条件的栈顶元素(<=nums[i]的)就出栈,直到符合条件就找到了答案。

| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | class Solution {public int[] dailyTemperatures(int[] tem) {
 
 
 
 
 
 
 
 
 
 int n = tem.length;
 int[] res = new int[n];
 
 Deque<Integer> stack = new LinkedList<>();
 for (int i = n - 1; i >= 0; i--) {
 
 while (!stack.isEmpty() && tem[i] >= tem[stack.peekFirst()]) stack.pollFirst();
 
 res[i] = stack.isEmpty() ? 0 : stack.peekFirst() - i;
 
 stack.addFirst(i);
 }
 return res;
 }
 }
 
 | 
2.基准元素nums[i] 在栈中的写法(23ms/56.8MB)
举个🌰:现在要寻找的nums[i]右边首个严格大于nums[i]的元素,st = [8, 6, 4, 3]
现在我们栈中的元素还要找自己右边首个大于自己的元素呢,嗷嗷待哺~~~
若nums[i]=2,比栈顶还小,直接入栈得了,所有人都还没找到比自己小的元素~~~此时 st = [8, 6, 4, 3, 2]
此时来了nums[i]=4,栈上方的2与3显然是找到了右边首个比自己严格大的元素,出栈并记录
但是栈中的4还是没有找到比自己严格大的,因此只能期待接下来有没有更大的数字了~~~
此时st = [8, 6, 4, 4] 若此时再来个5那么这两个可怜的4就有机会出栈并记录了~~~
要点: 如果找nums[i] 右边的元素,那么从左边遍历起以便于基准元素nuns[i]先入栈;
反之,如果找nums[i] 左边的元素,那么从右遍历起以便于基准元素nums[i]先入栈;
当前遍历到的nums[i]就是基准元素,且一直要保持栈顶是最小的,一旦新来的nums[i]严格大于栈顶的小可怜,就代表栈顶的小可怜可以出栈记录了
代码如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | public int[] dailyTemperatures(int[] tem) {
 
 
 
 
 
 
 
 int n = tem.length;
 int[] res = new int[n];
 
 Deque<Integer> stack = new LinkedList<>();
 for (int i = 0; i < n; i++) {
 
 while (!stack.isEmpty() && tem[i] > tem[stack.peekFirst()]) {
 int poll = stack.pollFirst();
 res[poll] = i - poll;
 }
 
 stack.addFirst(i);
 }
 return res;
 }
 
 | 
由运行结果可知两种写法时间消耗与空间消耗接近,个人比较推荐写法1
3.API的选用习惯:
目前比较推荐以下两种:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | 
 
 Deque<Integer> st = new LinkedList<>();
 st.push(i);
 st.peek();
 st.pop();
 
 
 
 
 Deque<Integer> st = new LinkedList<>();
 st.addFirst(i);
 st.peekFirst();
 st.pollFirst();
 
 | 
4.单调栈与范围管辖问题(Hard)
2104. 子数组范围和
| 12
 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
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 
 | int[] nums;int n;
 
 public long subArrayRanges(int[] _nums) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 nums = _nums;
 n = nums.length;
 long res = 0;
 
 int[] max = getCnt(true), min = getCnt(false);
 
 for (int i = 0; i < n; i++) {
 res += (long) (max[i] - min[i]) * nums[i];
 }
 return res;
 }
 
 
 private int[] getCnt(boolean isMax) {
 
 int[] ans = new int[n];
 
 
 int[] left = new int[n], right = new int[n];
 
 Deque<Integer> stack = new LinkedList<>();
 for (int i = 0; i < n; i++) {
 while (!stack.isEmpty() && (isMax ? nums[i] >= nums[stack.peekFirst()] : nums[i] <= nums[stack.peekFirst()])) stack.pollFirst();
 left[i] = stack.isEmpty() ? -1 : stack.peekFirst();
 stack.addFirst(i);
 }
 stack.clear();
 for (int i = n - 1; i >= 0; i--) {
 while (!stack.isEmpty() && (isMax ? nums[i] > nums[stack.peekFirst()] : nums[i] < nums[stack.peekFirst()])) stack.pollFirst();
 right[i] = stack.isEmpty() ? n : stack.peekFirst();
 stack.addFirst(i);
 }
 
 for (int i = 0; i < n; i++) {
 ans[i] = (i - left[i]) * (right[i] - i);
 }
 return ans;
 }
 
 | 
2334. 元素值大于变化阈值的子数组
| 12
 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
 36
 37
 38
 39
 40
 41
 42
 43
 
 | class Solution {public int validSubarraySize(int[] nums, int threshold) {
 
 
 
 
 
 
 
 
 
 
 int n = nums.length;
 
 Deque<Integer> st1 = new LinkedList<>(), st2 = new LinkedList<>();
 
 
 int[] left = new int[n], right = new int[n];
 
 for (int i = 0; i < n; i++) {
 
 
 
 
 
 while (!st1.isEmpty() && nums[st1.peek()] >= nums[i]) st1.pop();
 left[i] = st1.isEmpty() ? -1 : st1.peek();
 st1.push(i);
 }
 
 for (int i = n - 1; i >= 0; i--) {
 while (!st2.isEmpty() && nums[st2.peek()] >= nums[i]) st2.pop();
 right[i] = st2.isEmpty() ? n : st2.peek();
 st2.push(i);
 }
 
 for (int i = 0; i < n; i++) {
 int k = right[i] - left[i] - 1;
 if (nums[i] > threshold / k) return k;
 }
 return -1;
 }
 }
 
 | 
5.栈结构写法的优化
以2104. 子数组范围和为例进行说明
栈结构还可以优化成数组的写法,辅助一个ptr指针,在数据范围小的时候非常节省时间与空间
其中ptr指代当前栈中栈顶元素所在stack数组的位置
prt指针可以初始化为-1或者0表示栈底索引,栈的容量初始化为可能出现的最大栈中元素个数
入栈: stack[++ptr]=i 腾出空位再赋值
出栈: 直接ptr–即可,后面新元素入栈会覆盖掉旧的痕迹
查看栈顶元素: 返回stack[ptr]就是
清空栈: ptr=0(-1)
| 12
 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
 36
 37
 38
 39
 40
 41
 42
 43
 
 | int[] nums;int n;
 
 public long subArrayRanges(int[] _nums) {
 nums = _nums;
 n = nums.length;
 long res = 0;
 
 int[] max = getCnt(true), min = getCnt(false);
 
 for (int i = 0; i < n; i++) {
 res += (long) (max[i] - min[i]) * nums[i];
 }
 return res;
 }
 
 
 private int[] getCnt(boolean isMax) {
 
 int[] ans = new int[n];
 
 
 int[] left = new int[n], right = new int[n];
 
 int[] stack = new int[n + 1];
 int ptr = 0;
 for (int i = 0; i < n; i++) {
 while (ptr > 0 && (isMax ? nums[i] >= nums[stack[ptr]] : nums[i] <= nums[stack[ptr]])) ptr--;
 left[i] = ptr == 0 ? -1 : stack[ptr];
 stack[++ptr] = i;
 }
 ptr = 0;
 for (int i = n - 1; i >= 0; i--) {
 while (ptr > 0 && (isMax ? nums[i] > nums[stack[ptr]] : nums[i] < nums[stack[ptr]])) ptr--;
 right[i] = ptr == 0 ? n : stack[ptr];
 stack[++ptr] = i;
 }
 
 for (int i = 0; i < n; i++) {
 ans[i] = (i - left[i]) * (right[i] - i);
 }
 return ans;
 }
 
 | 
6.记录的一些练习题
给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。
请你以字符串形式返回这个最小的数字。

思路:
单调栈:维护一个栈底->栈顶单调递增的栈(参考"笨猪爆破组"题解)
要尽量让高位维护一个递增的趋势,即去除高位的降序对
如:1234532与5321234,当组成数字相同时,必定是前面高位递增数值会更加小
现在任务变成:删除k位使得高位的递增序列尽量长(删除高位尽可能多的递减对)
单调栈按顺序存储想要的数字,其中栈顶为栈中最大值,将当前遍历到的数字与栈顶比较决定栈顶的去留
设栈顶数字为B,当前遍历到的数字为A:
1.B > A:B与A之间组成递减对,此时去掉B,A代替B的位置可以使得数值变小
2.B <= A:B与A之间组成递增对,让A入栈是最好的选择
这里要注意几个问题:
1.能去除(出栈)的数字个数有限,不能超过k,因此超过k部分直接加入,不保证栈的单调性
2.考虑去除前导0的情况,前导0不能加入;换句话说就是:非0和有垫底的0可以加入
3.当前num中的降序对 < k时,删除完所有降序对后,num已经是递增,此时截取前面部分就是最小的;
4.当前num中的降序对 >= k时,恰好删除完或者有剩余,此时num后面的直接加入即可,不用考虑保持栈的单调性
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | class Solution {public String removeKdigits(String num, int k) {
 if (num.length() == k) return "0";
 StringBuilder sb = new StringBuilder();
 
 Deque<Character> stack = new LinkedList<>();
 
 for (char ch : num.toCharArray()) {
 
 while (k > 0 && !stack.isEmpty() && stack.peekFirst() > ch) {
 
 stack.pollFirst();
 k--;
 }
 
 if(ch != '0' || !stack.isEmpty()) stack.addFirst(ch);
 }
 
 while (k-- > 0 && !stack.isEmpty()) stack.pollFirst();
 while (!stack.isEmpty()) sb.append(stack.pollFirst());
 
 return sb.length() == 0 ? "0" : sb.reverse().toString();
 }
 }
 
 | 
给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。
需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

思路:
单调栈+标记:
参考labuladong的题解,本题的关键点有几个:
1.要求最后的字母没有重复,且每个字母都要出现一次
2.要按照原来字母的出现顺序输出(相对顺序不变)
3.满足条件1与2前提下的要求返回字符串的字典序最小
其中,条件1可以用boolean数组或者HashSet等数据结构来进行存储,如果前面出现过就不加入;
条件2要求相对顺序不变,可以用一个指针进行遍历再存储,只要满足数据存储有序的数据结构即可
最关键的是条件3,要求字典序最小的情形输出。这里就是要用到单调栈最深层的原因
因为我们要操作"临近元素"(降序对)!为什么要操作降序对?
这个可以参考 402. 移掉 K 位数字(中等)
402题是要求删掉k个数字使得最后的结果最小,核心就是优先找出"降序对",例如:76,98,54…
因为当你删除了降序对前面的数字后,后面数字的居上取代了原来前面的数字,必定使得数字变小
A=123a56与B=123b56,a与b当前面完全一致,整个数字大小取决于a与b的大小,a>b则A>B
回到本题中,我们要使得字典序在满足其他条件的前提下尽可能小,那么可以选择删掉降序对的前面字母
由于后来居上的特征,降序对是不断变化的,因此要用栈这种结构来模拟,每当当前字母ch满足其他条件时,
如果与栈顶组成降序对,说明我现在有机会让字典序变小,因此必然会想着将栈顶元素弹出!
可是本题要求所有字母都要出现一次,你把栈顶弹出了之后万一后面不再出现栈顶的字母不就不满足题意?
因此还要看看后面有没有再出现栈顶元素:
1.若没有出现则不能弹出,ch乖乖进去吧!
2.若后面还会出现,可以放心弹出,后面还可以加入该字母的,这样既使得字典序尽量小又能满足条件!
做法就是用count数组维护当前位置后面剩余的每个字母个数,当个数减为0时说明后面没有了!
| 12
 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
 36
 37
 38
 39
 40
 
 | class Solution {public String removeDuplicateLetters(String s) {
 StringBuilder sb = new StringBuilder();
 
 int[] count = new int[26];
 for (char c : s.toCharArray()) {
 count[c - 'a']++;
 }
 
 boolean[] inStack = new boolean[26];
 
 Deque<Character> stack = new LinkedList<>();
 
 for (char c : s.toCharArray()) {
 
 count[c - 'a']--;
 
 if(inStack[c - 'a']) continue;
 
 while (!stack.isEmpty() && c < stack.peekFirst()) {
 
 if(count[stack.peekFirst() - 'a'] == 0) break;
 
 char poll = stack.pollFirst();
 
 inStack[poll - 'a'] = false;
 }
 
 stack.addFirst(c);
 
 inStack[c - 'a'] = true;
 }
 
 while (!stack.isEmpty()) {
 sb.append(stack.pollFirst());
 }
 
 return sb.reverse().toString();
 }
 }
 
 | 
面试题 03.02. 栈的最小值 同 155.最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
MinStack() 初始化堆栈对象。
void push(int val) 将元素val推入堆栈。
void pop() 删除堆栈顶部的元素。
int top() 获取堆栈顶部的元素。
int getMin() 获取堆栈中的最小元素。

思路:
单调递增栈(栈顶->栈底)维护栈中元素最小值:
方法1:完全同步栈:一个为常规栈(主栈);另一个为同步栈,保存每一步中主栈元素最小值
这个方法要求主栈与同步栈的元素个数严格相等,加入时两个同步加入,弹出时两个同步弹出,无需多加判断
这种方法存在一定的冗余存储,于是就有了方法2
方法2:主栈+单调栈:一个为主栈,另一个为单调递增栈(栈顶->栈底),当且仅当要加入的元素x<=单调栈栈顶时才加入
其余情况不加入;同理,弹出时要看单调栈栈顶是否与主栈弹出的元素相等,若相等的话就弹出。
注意:这个单调栈是非主动挤压型的,也就是在加入元素时,如果当前元素不能与现在的单调栈构成单调递增栈会主动放弃,而不会将栈中不符合条件的全部弹出(这显然会损失掉部分最小值信息!)
| 12
 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
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 
 | class MinStack_1 {Deque<Integer> stack;
 Deque<Integer> minStack;
 
 public MinStack_1() {
 stack = new LinkedList<>();
 minStack = new LinkedList<>();
 
 minStack.addFirst(Integer.MAX_VALUE);
 }
 
 
 public void push(int x) {
 
 stack.addFirst(x);
 
 int min = Math.min(minStack.peekFirst(), x);
 minStack.addFirst(min);
 }
 
 
 public void pop() {
 
 int pop = stack.pollFirst();
 
 minStack.pollFirst();
 }
 
 
 public int top() {
 return stack.peekFirst();
 }
 
 
 public int getMin() {
 return minStack.peekFirst();
 }
 }
 ----------------------------------------------------------------------------------------------------------------
 class MinStack_2 {
 Deque<Integer> stack;
 Deque<Integer> minStack;
 
 public MinStack_2() {
 stack = new LinkedList<>();
 minStack = new LinkedList<>();
 
 minStack.addFirst(Integer.MAX_VALUE);
 }
 
 
 public void push(int x) {
 
 stack.addFirst(x);
 
 if(x <= minStack.peekFirst()) minStack.addFirst(x);
 }
 
 
 public void pop() {
 
 int pop = stack.pollFirst();
 
 if(minStack.peekFirst() == pop) minStack.pollFirst();
 }
 
 
 public int top() {
 return stack.peekFirst();
 }
 
 
 public int getMin() {
 return minStack.peekFirst();
 }
 }
 
 | 
这道题要时刻注意判空异常,还有初始化辅助栈要将不影响首个元素入栈的Integer.MAX_VALUE加入!!!记住了