前言
单调栈是一种基于栈的结构,基本操作包括弹栈出栈,但是需要维持栈中的数据是严格有序的。
- 单调递增栈:从栈底到栈顶是从大到小
- 单调递减栈:从栈底到栈顶是从小到大
这就需要我们在栈基本操作的基础上进行一些逻辑的控制。这样的一个数据结构在解决某些问题时会变得非常高效。
单调栈介绍
单调栈操作过程:
- 当栈为空时,元素直接压栈
- 当栈不为空时,比较压入的元素和栈顶元素,如果插入的元素大于(或者小于)栈顶,那么就直接压入;
- 如果不大于(或者小于),那么就一直弹栈,知道栈顶元素小于(或者大于)待插入的元素时,压入该元素。
如何理解单调栈
单调栈个人认为比较强调这种思想,首先,栈内是有序的。这种有序的结构是依靠着将破坏有序顺序的元素弹出,知道压入的新元素继续保持有序。
单调栈可以处理单个数组,如果问题涉及多个,可能还需要借助其他数据结构。
单调栈是一种理解起来很简单,但是不好去使用的技巧。
单调栈被用于解决某类典型的问题:下一个最大值问题。即在数组中找出当前位置之后的大于它的数。
其过程可以理解成如下图:
就像排座位一样,坐在一排的同学,会被在坐在自己前面的同学挡住,第一个挡住你的同学位置就是这类问题的解。
站在1
的位置向后看,第一个大于它的数在3
。2
位置比1
小,所以不在1
的视野内。其他数字也是如此操作的。
对应的栈操作就是:
- 栈为空,1位置数字压栈。
- 指针来到2位置,arr[2] = 1 < 3 ,压栈。
- 指针来到3位置,arr[3] = 4 > 1, 3位置就是第一个大于2位置的下标,弹栈。因为弹出2位置一定不是第一个大于1位置的数字下标,且2位置只是作为1的右边的位置,其他数不需要考虑2位置上的数字, 所以弹出它对其他位置没有任何影响,且2位置的结果也已经正确返回。
- 此时还要判断当前位置和栈顶的数字的关系,发现3位置也比1位置大,那么3位置也是1位置的结果。弹出1位置,此时栈空,压入3位置,本次循环体结束。
Java代码:
import java.util.Stack;
/**
* 单调递增栈Java实现
*/
public class OrderedStack<T> {
Stack<T> element= null;
public OrderedStack() {
element = new Stack();
}
public void push(T t) {
// 自定义比较规则
while (!isEmpty() && peek().equals(t)) pop();
element.push(t);
}
public T pop() {
return element.pop();
}
public T peek() {
return element.peek();
}
public boolean isEmpty() {
return element.isEmpty();
}
}
应用
找出下一个最大的元素
给定两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。找到 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
思路
先利用单调栈在nums2中找出所有位置的的下一个最大元素,因为nums1是nums2的子集,所以我们找出的结果(nums2的)一定是包括了nums1的结果,利用hashMap
就可以找出对应的结果。
import java.util.Stack;
import java.util.HashMap;
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Stack<Integer> stack = new Stack<>();
HashMap<Integer,Integer> hm = new HashMap();
int[] res = new int[nums1.length];
for (int i = 0; i < nums2.length; i++) {
// 单调栈操作
while (!stack.isEmpty() && stack.peek() < nums2[i]) {
hm.put(stack.pop(),nums2[i]);
}
stack.push(nums2[i]);
}
while (!stack.isEmpty()) {
hm.put(stack.pop(),-1);
}
for (int i = 0; i < res.length; i++) {
res[i] = hm.get(nums1[i]);
}
return res;
}
}
每日温度
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
思路
和问题1完全一致
import java.util.Deque;
import java.util.LinkedList;
class Solution {
public int[] dailyTemperatures(int[] T) {
int[] res = new int[T.length];
Deque<Integer> stack = new LinkedList<>();
for (int i = 0; i < T.length; i++) {
while (!stack.isEmpty() && T[i] > T[stack.peek()])
res[stack.peek()] = i - stack.pop();
stack.push(i);
}
return res;
}
}
找出下一个最大的元素2
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第
一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
思路
循环数组导致:每个位置的结果可能是在其后面,也可能是在其前面,因为是循环的嘛。
依旧利用单调栈,不过需要模拟出循环的过程,利用 i % nums.length
可以达到循环的效果。
import java.util.Stack;
class Solution {
public int[] nextGreaterElements(int[] nums) {
int[] res = new int[nums.length];
Stack<Integer> stack = new Stack<>();
int n = nums.length;
for (int i = 0; i < n * 2; i++) {
// 实现循环
int num = nums[i % n];
// 单调栈操作, 注意,当num = 栈顶时说明从当前位置已经走了一圈回来了,说明此时就不需要在往后判断了
while (!stack.isEmpty() && num > nums[stack.peek()])
res[stack.pop()] = num;
// 当i大于n时就不要在push了,会导致重复
if (i < n) stack.push(i);
}
while (!stack.isEmpty())
res[stack.pop()] = -1;
return res;
}
}
找出元素前后第一个大于它的数
给定一个数组arr, 找到每一个i位置左边和右边离其最近且小于它的数。如果没有就返回-1。
思路
和单调栈的思路完全一致,当一个数从栈中弹出时,它的弹出是由于压入的数小于它导致的:
1. 那么这个压入的数就是其右边第一个比他小的数。
2. 他下面的数就是左边第一个比他小的数。
当数组遍历完成,如果栈不为空,那么弹出的数的右边一定没有比他小的数,左边比他小的数在它的下面(栈里)。
import java.util.Stack;
class Solution {
public int[][] GreaterElements(int[] arr) {
Stack<Integer> stack = new Stack<>();
int[][] res = new int[arr.length][2];
for (int i = 0; i < arr.length; i++) {
while (!stack.isEmpty() && arr[stack.peek()] < arr[i]) {
int index = stack.pop();
res[index][1] = arr[i];
int num = stack.isEmpty() ? -1 : arr[stack.peek()];
res[index][0] = num;
}
stack.push(i);
}
while (!stack.isEmpty()) {
int index = stack.pop();
res[index][1] = -1;
int num = stack.isEmpty() ? -1 : arr[stack.peek()];
res[index][0] = num;
}
return res;
}
}
参考
https://labuladong.gitbook.io/algo/shu-ju-jie-gou-xi-lie/dan-tiao-zhan
《程序员代码面试指南(第二版)》