栈(一)——单调栈

前言

单调栈是一种基于栈的结构,基本操作包括弹栈出栈,但是需要维持栈中的数据是严格有序的。

  1. 单调递增栈:从栈底到栈顶是从大到小
  2. 单调递减栈:从栈底到栈顶是从小到大

这就需要我们在栈基本操作的基础上进行一些逻辑的控制。这样的一个数据结构在解决某些问题时会变得非常高效。

单调栈介绍

单调栈操作过程:

  1. 当栈为空时,元素直接压栈
  2. 当栈不为空时,比较压入的元素和栈顶元素,如果插入的元素大于(或者小于)栈顶,那么就直接压入;
  3. 如果不大于(或者小于),那么就一直弹栈,知道栈顶元素小于(或者大于)待插入的元素时,压入该元素。

如何理解单调栈

单调栈个人认为比较强调这种思想,首先,栈内是有序的。这种有序的结构是依靠着将破坏有序顺序的元素弹出,知道压入的新元素继续保持有序。
单调栈可以处理单个数组,如果问题涉及多个,可能还需要借助其他数据结构。
单调栈是一种理解起来很简单,但是不好去使用的技巧。

单调栈被用于解决某类典型的问题:下一个最大值问题。即在数组中找出当前位置之后的大于它的数。

其过程可以理解成如下图:
在这里插入图片描述
就像排座位一样,坐在一排的同学,会被在坐在自己前面的同学挡住,第一个挡住你的同学位置就是这类问题的解。

站在1的位置向后看,第一个大于它的数在32位置比1小,所以不在1的视野内。其他数字也是如此操作的。
对应的栈操作就是:

  1. 栈为空,1位置数字压栈。
  2. 指针来到2位置,arr[2] = 1 < 3 ,压栈。
  3. 指针来到3位置,arr[3] = 4 > 1, 3位置就是第一个大于2位置的下标,弹栈。因为弹出2位置一定不是第一个大于1位置的数字下标,且2位置只是作为1的右边的位置,其他数不需要考虑2位置上的数字, 所以弹出它对其他位置没有任何影响,且2位置的结果也已经正确返回。
  4. 此时还要判断当前位置和栈顶的数字的关系,发现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

《程序员代码面试指南(第二版)》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值