为什么要汇总在一块?
三者都有何区别?
总结
1 泛化性更好的策略
个人建议单调栈/队列中存放的元素最好是下标而不是值,因为有的题目需要根据下标计算,这样泛化性更好。参考lc239和lc496
2 单调队列何栈其实可以共用同一套模板
大概的思路是先把不满足单调性的元素poll掉,然后offer一个当前符合条件的元素。参考lc239和lc496
3 单调栈专门解决Next Greater Number问题
1 单调队列
239. 滑动窗口最大值(这个题中要求存储元素下标,因为需要根据下标判断是否在窗口内)
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer>q=new ArrayDeque<>();
int[] res=new int[nums.length-k+1];
int index=0;
for(int i=0;i<nums.length;i++){
while(!q.isEmpty()&&q.peekFirst()<i-k+1){
q.pollFirst();
}
while(!q.isEmpty()&&nums[q.peekLast()]<nums[i]){
q.removeLast();
}
q.offer(i);
if(i>=k-1){
res[index++]=nums[q.peekFirst()];
}
}
return res;
}
2023.7 用友提前批秋招笔试
// 说明:这里使用两个堆来维护最大值和最小值
// 代码简单易懂,笔试时是一个速AC的好方法,
// 时间复杂度O(N*logK),比起单调队列的O(N)的复杂度,是差了一些, 另外这道题是力扣
239. 滑动窗口最大值
的变体,本体想到的另一种方法是同时维护最大值,次大值,最小值和次小值,每次滑动时看弹出的那个是否最大/小,否则就看是否次大/小,然后进行相应的替换
import java.util.*;
public class Main{
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int k=sc.nextInt();
int[]arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=sc.nextInt();
}
PriorityQueue<Integer>maxH=new PriorityQueue<>((o1,o2)->(o2-o1));
PriorityQueue<Integer>minH=new PriorityQueue<>((o1,o2)->(o1-o2));
for(int i=0;i<k;i++){
maxH.add(arr[i]);
minH.add(arr[i]);
}
int res=maxH.peek()-minH.peek();
for(int i=k;i<n;i++){
int pollE=arr[i-k];
maxH.remove(pollE);
minH.remove(pollE);
maxH.add(arr[i]);
minH.add(arr[i]);
res=Math.max(res,maxH.peek()-minH.peek());
}
System.out.println(res);
}
}
方法二:(平常要了解,但是笔试不推荐做,不如使用两个堆来得简单)
采用单调队列来做,时间复杂度O(n)
public static void main(String[]args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int k=sc.nextInt();
int[]arr=new int[n];
for(int i=0;i<n;i++){
arr[i]=sc.nextInt();
}
Deque<Integer>maxQ=new ArrayDeque<>();
Deque<Integer>minQ=new ArrayDeque<>();
int res=0;
for(int i=0;i<n;i++){
while(!maxQ.isEmpty()&&maxQ.peekFirst()<i-k+1){
maxQ.pollFirst();
}
while(!minQ.isEmpty()&&minQ.peekFirst()<i-k+1){
minQ.pollFirst();
}
while(!maxQ.isEmpty()&&arr[maxQ.peekLast()]<arr[i]){
maxQ.pollLast();
}
while(!minQ.isEmpty()&&arr[minQ.peekLast()]>arr[i]){
minQ.pollLast();
}
maxQ.offer(i);
minQ.offer(i);
res=Math.max(res,arr[maxQ.peekFirst()]-arr[minQ.peekFirst()]);
}
System.out.println(res);
}
单调队列是一个非常有用的数据结构,可以解决一类特定的问题,主要是和数组中的滑动窗口最值问题有关。以下是几个使用单调队列的LeetCode题目:
- LeetCode 239. 滑动窗口最大值:给你一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。
- LeetCode 155. 最小栈:设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
- LeetCode 907. 子数组的最小值之和:给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。
- LeetCode 862. 和至少为 K 的最短子数组:返回 A 的最短的非空连续子数组的长度,该子数组的和至少为 K 。如果没有和至少为 K 的非空子数组,返回 -1 。
- LeetCode 456. 132 模式:给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]、nums[j] 和 nums[k] 组成,并同时满足:i < j < k 和 nums[i] < nums[k] < nums[j] 。如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false 。
这些题目都可以使用单调队列来优化时间复杂度。
2 单调栈
496. 下一个更大元素 I
思路:求下一个元素就需要先知道后面的元素才能做判断,由此我们可以先从数组尾部开始向前遍历,,因为求的是更大的元素,后面的元素如果更大则一定是在栈底,所以是一个单调递减栈,如果是栈是单调递减的,则在出入栈的时候顺便可以根据栈中是否为空做出判断,如果为空则说明当前的值没有下一个更大值。
代码:
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Map<Integer,Integer>mp=new HashMap<>();
Deque<Integer>st=new ArrayDeque<>();
for(int i=nums2.length-1;i>=0;i--){
int num=nums2[i];
while(!st.isEmpty()&&num>st.peekLast()){
st.pollLast();
}
mp.put(num,st.isEmpty()?-1:st.peekLast());
st.offerLast(num);
}
int[]res=new int[nums1.length];
for(int i=0;i<nums1.length;i++){
res[i]=mp.get(nums1[i]);
}
return res;
}
// 栈中存入下标,泛化性更好的答案
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
HashMap<Integer,Integer> map = new HashMap<>();
Deque<Integer> stack = new LinkedList<>();
for(int i=0;i<nums2.length;i++){
while(!stack.isEmpty()&&nums2[stack.peek()]<nums2[i]){
int j = stack.pop();
map.put(nums2[j],nums2[i]); // 此时nums2[j]<nums2[i]
}
stack.push(i); // 下标入栈
}
int[] ans = new int[nums1.length];
for(int i=0;i<nums1.length;i++){
ans[i] = map.getOrDefault(nums1[i],-1);
}
return ans;
}
}
503.下一个更大元素II
class Solution {
public int[] nextGreaterElements(int[] nums) {
int n=nums.length;
Deque<Integer>st=new ArrayDeque<>();
int[]res=new int[n];
for(int i=2*n-1;i>=0;i--){
int newI=i%n;
while(!st.isEmpty()&&nums[st.peekLast()%n]<=nums[newI]){ //#A
st.pollLast();
}
res[i%n]=st.isEmpty()?-1:nums[st.peekLast()%n];//#B
st.offerLast(i);
}
return res;
}
}
问:为这里的边界条件中,为什么pop的时候是nums[st.peekLast()%n]<=nums[newI]而不是nums[st.peekLast()%n]<=nums[newI]?
答:因为题目求解的是下一个更大的元素,而不是下一个不比自己小的元素,如果改成了后者,则在#B做判断时,会可能取到一个跟自己一样大的元素。比如 1 2 2,如果使用后者,两个2会连续入栈,否则只能入栈一次。
907. 子数组的最小值之和
正常思路
每遍历一个元素就以当前元素为基点,向左右扩散,找到一个自己是最小值的最长子数组,然后计算这个最长子数组可以分成多少个包含当前元素的子数组,计算个数*最小值然后累加到结果中。
public int sumSubarrayMins3(int[] arr) {
int n=arr.length;
long res=0;
final int mod=1000000007;
for(int i=0;i<n;i++){
int l=i,r=i;
//找到左边界
while(l-1>=0&&arr[l-1]>=arr[i]){
l--;
}
//找到右边界,为什么右边界是>而不是>=
while(r+1<n&&arr[r+1]>arr[i]){
r++;
}
long numSubArr=(i-l+1)*(r-i+1);
// System.out.println("i:"+i+",l:"+l+",r:"+r+",numSubArr:"+numSubArr+",tmpSum:"+numSubArr*arr[i]);
res=(res+numSubArr*arr[i])%mod;
}
return (int)(res%mod);
}
单调栈解题
单调栈解决的就是找出”下一个更大值“的问题,在本题中,寻找当前元素的左边界等同于上一个更大值(从左往右遍历),寻找右边界相当于下一个更大值,要求从结尾往前遍历数组
public int sumSubarrayMins(int[] arr) {
int n=arr.length;
//从前往后求上一个更大值
//递减栈
Deque<Integer>st1=new ArrayDeque<>();
int[]l=new int[n];
for(int i=0;i<n;i++){
while(!st1.isEmpty()&&arr[st1.peekLast()]>arr[i]){
st1.pollLast();
}
l[i]=i-(st1.isEmpty()?-1:st1.peekLast());
st1.offerLast(i);
}
//从后往前求下一个更大值
//递减栈
Deque<Integer>st2=new ArrayDeque<>();
int[]r=new int[n];
for(int i=n-1;i>=0;i--){
while(!st2.isEmpty()&&arr[st2.peekLast()]>=arr[i]){
st2.pollLast();
}
r[i]=(st2.isEmpty()?n:st2.peekLast())-i;
st2.offerLast(i);
}
long res=0;
final int mod=1000000007;
for(int i=0;i<n;i++){
res=(res+(long)arr[i]*r[i]*l[i])%mod;
}
return (int)res;
}
为什么在找边界时,左边界的while循环条件是arr[st2.peekLast()]>arr[i]但是查找右边界时是arr[st2.peekLast()]>=arr[i]?
答:这是为了区分重复的元素的边界,比如4232,如果都是大于号,则子集找不全,如果都是大于等于号,则会有冗余,比如第一个2的所属子集{42,423,23,232,2,4232}和第二个2的所属子集{32, 2,232,4232}的重合子集是{232,4232},但是事实上如果对第一个使用>,第二个使用>=,则子集集合分别为{42,423,23,232,2,4232},{32,2}
1118.一月有多少天
3 辅助栈
辅助栈解法 155. 最小栈
class MinStack {
Deque<Integer>st;
Deque<Integer>q;
public MinStack() {
st=new ArrayDeque<>();
q=new ArrayDeque<>();
}
public void push(int val) {
st.offerLast(val);
if(q.isEmpty()||val<q.peekLast()){
q.offerLast(val);
}else{
q.offerLast(q.peekLast());
}
}
public void pop() {
st.pollLast();
q.pollLast();
}
public int top() {
return st.peekLast();
}
public int getMin() {
return q.peekLast();
}
}
类似于辅助栈的解法
辅助栈的思想无非就是将每个状态下的最小值存储了起来,我们可以使用类或结构体等自定义类型作为栈的元素,在这个元素中,我们记录当前状态下存储最小值的元素的下标,这样我们就不需要辅助栈了
class MinStack {
class Node{
int val;
int minIndex;
Node(int val,int minIndex){
this.val=val;
this.minIndex=minIndex;
}
Node(int val){
this.val=val;
}
}
List<Node> stack;
public MinStack() {
stack=new ArrayList<>();
}
public void push(int val) {
if (stack.isEmpty()){
stack.add(new Node(val, 0));
}
else {
int currMinIndex=stack.get(stack.size()-1).minIndex;
int minIndex;
if (stack.get(currMinIndex).val>val){
minIndex=stack.size();
}
else {
minIndex=currMinIndex;
}
stack.add(new Node(val,minIndex));
}
}
public void pop() {
stack.remove(stack.size()-1);
}
public int top() {
return stack.get(stack.size()-1).val;
}
public int getMin() {
return stack.get(stack.get(stack.size()-1).minIndex).val;
}
}