单调栈介绍
单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。
听起来有点像堆(heap)?不是的,单调栈用途不太广泛,只处理一种典型的问题,叫做 Next Greater Element。
代码模板:
public int[] nextGreaterElement(int[] nums) {
int len = nums.length;
int[] res = new int[len];
Stack<Integer> s = new Stack<>();
for (int i = len-1; i >= 0 ; i--) {
while (!s.isEmpty() && s.peek()<=nums[i]) {
s.pop();
}
res[i] = s.isEmpty()? -1:s.peek();
s.push(nums[i]);
}
return res;
}
单调栈典型例题
- 【力扣-496. 下一个更大元素 I】
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int len1 = nums1.length, len2 = nums2.length;
Map<Integer, Integer> map = new HashMap<>();
int[] res = new int[len2];
Stack<Integer> s = new Stack<>();
for (int i = len2-1; i >= 0 ; i--) {
while (!s.isEmpty() && s.peek()<=nums2[i]) {
s.pop();
}
res[i] = s.isEmpty()? -1:s.peek();
s.push(nums2[i]);
}
for (int i = 0; i < len2; i++) {
map.put(nums2[i], res[i]);
}
int[] ans = new int[len1];
for (int i = 0; i < len1; i++) {
ans[i] = map.get(nums1[i]);
}
return ans;
}
}
- 【力扣-503. 下一个更大元素 II】
class Solution {
public int[] nextGreaterElements(int[] nums) {
int len = nums.length;
Stack<Integer> stack = new Stack<>();
int[] res = new int[len];
for (int i = 2*len-1; i >= 0 ; i--) {
while (!stack.isEmpty() && stack.peek() <= nums[i%len] ) {
stack.pop();
}
res[i%len] = stack.isEmpty()? -1:stack.peek();
stack.push(nums[i%len]);
}
return res;
}
}
- 【力扣-739. 每日温度】
class Solution {
public int[] dailyTemperatures(int[] T) {
int len = T.length;
int[] res = new int[len];
Stack<Integer> stack = new Stack<>();
for (int i = len-1; i >= 0 ; i--) {
while (!stack.isEmpty() && T[stack.peek()] <= T[i]) {
stack.pop();
}
res[i] = stack.isEmpty()? 0:stack.peek()-i;
stack.push(i);
}
return res;
}
}
单调队列介绍
也许这种数据结构的名字你没听过,其实没啥难的,就是一个「队列」,只是使用了一点巧妙的方法,使得队列中的元素全都是单调递增(或递减)的。
「单调栈」主要解决 Next Great Number 一类算法问题,而「单调队列」这个数据结构可以解决滑动窗口相关的问题,比如说力扣第 239 题「滑动窗口最大值」,难度 Hard:
单调队列经典例题
- 【力扣-239. 滑动窗口最大值】
class MonotonicQueue {
LinkedList<Integer> list;
public MonotonicQueue() {
this.list = new LinkedList<>();
}
public void push(int num) {
while (!list.isEmpty() && list.getLast() < num) {
list.pollLast();
}
list.addLast(num);
}
public int getMax() {
return list.getFirst();
}
public void pop(int num) {
if (num == list.getFirst()) {
list.pollFirst();
}
}
}
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
MonotonicQueue mq = new MonotonicQueue();
List<Integer> al = new ArrayList<>();
for (int i=0; i<nums.length; i++) {
if (i < k-1) {
mq.push(nums[i]);
} else {
mq.push(nums[i]);
al.add(mq.getMax());
mq.pop(nums[i+1-k]);
}
}
int[] ans = new int[nums.length+1-k];
for (int i = 0; i < ans.length; i++) {
ans[i] = al.get(i);
}
return ans;
}
}
总结:
这道题直观的解法是用优先队列,但是用单调队列可以在
O
(
1
)
O(1)
O(1) 时间算出每个「窗口」中的最大值,使得整个算法在线性时间完成。
尝试用单调队列来做,这样的好处是单调队列插入、删除、获取最大值的时间复杂度
均是O(1),相对于用优先队列(大顶堆)来说更快!
击败从12%提升到了38%,还是少得可怜~
两次提交语法报错的原因分别在于:
1. push方法中,poll默认弹出头结点,所以这儿应该用pollLast
2. while (!list.isEmpty() && list.getLast() < num) 中为<,而不是<=