算法学习随记 - 单调栈

记录单调栈学习笔记,以下几道力扣题为例子。
第84、 42、739、496、316、901、402、581 题。

单调栈中的元素具有单调性
单调栈分为: (1)单调递增栈 (2)单调递减栈

大体框架

   //创建栈结构,我一般使用的LinkedList作为栈来使用
   //常用的方法:push()入栈,pop()出栈,peek()获取栈顶元素的值
  stack =  new stack()for (遍历数组){//O(n)时间复杂度
  	  //当栈非空  &&   当前遍历的元素大于/小于栈顶元素,其中单调递增栈是“小于”,否则是“大于”。
  	  //将栈内不符合单调性的元素出栈,
      while (栈非空 && 当前遍历的元素大于/小于栈顶元素){
          栈顶元素 = stack.pop();//弹出栈顶元素
		  ...
		  这一部分是依照题意对栈顶元素的其他操作   
		  ....       
      }
      stack.push(数组当前遍历的元素);//当前元素入栈
  }

个人理解这几道题使用单调栈分别达到的效果:要么是找到某个符合条件可以到达的边界,要么保证得到的结果排列后“较小”

  • leetcode84(hard) 找到符合题意的数组每个元素的左右边界
  • leetcode42(hard) 某一根柱子所能接的水是由其左右边界界定的,使用单调栈求左右边界。
  • leetcode581 找到符合题意的 左右边界
  • leetcode739 计算数组内每一个元素符合题意的右边界
  • leetcode316 不是很“正宗”的单调栈记录结果,这一题栈中的元素不算是完全的单调,只是栈内元素部分单调,维持栈字典序较小的元素排在前面
  • leetcode901找小于或等于今天价格的最大连续日数,还是找当天符合题意的左边界
  • leetcode402利用单调递增栈将较小的值使得较小的值排在前面
  • leetcode581 根据题意找到需要排序子数组的左右边界

leetcode84 柱状图中最大矩形(困难)

原题链接

/**
 * 题目:给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
 * 求在该柱状图中,能够勾勒出来的矩形的最大面积。
 *
 * 整体思路:找到每一根柱子 【以当前柱子高度为高】所能向左右扩展得到的最大矩形 R。
 * R的求法:找到  当前柱子  左右两边  最接近的且高度小于当前柱子高度的   其他柱子的下标,就是以当前柱子为【高】可以扩展的最大矩形R的左右下标范围(R不包含范围两边界)
 * 其中如何找到左右两边 最接近当前柱子且高度小于当前柱子的下标呢?
 * 【首先假设存在两根柱子,其中j0,j1 分别是两根柱子的下标,j0= height[j1], 再假设往右还有某一根当前柱子下标为i (i>j1 ),那么其左边符合条件的柱子就是j1,不可能是j0,因为height[j1]已经小于height[i]了,j1就是i所能向左边扩展的最大矩形的左边界】  --> 使用单调栈的关键  --> O(N)的时间复杂度
 * 因此我们可以使用【单调栈】,使栈中的元素保持单调递增
 * 遍历时每次都将大于当前柱子高度的柱子出栈,再将当前柱子进栈。
 * @param heights
 * @return
 */
public int largestRectangleArea(int[] heights) {
    int len;
    if(heights==null || (len=heights.length)==0 ) return 0;
    int[] left = new int[len];//每根柱子可以扩展到的左边界
    int[] right = new int[len];//每根柱子可以扩展到的右边界
    LinkedList<Integer> stack = new LinkedList<>();//单调栈,保持栈内元素单调递增
    //计算左边界
    for (int i=0;i<len;i++){
        while (!stack.isEmpty() && heights[stack.peek()]>=heights[i]){
            stack.pop();
        }
        left[i] = stack.isEmpty()?-1:stack.peek();
        stack.push(i);
    }
    stack.clear();
    //计算右边界
    for (int i=len-1;i>=0;i--){
        while (!stack.isEmpty() && heights[stack.peek()]>=heights[i]){
            stack.pop();
        }
        right[i] = stack.isEmpty()?len:stack.peek();
        stack.push(i);
    }
    int largestArea = 0;
    for (int i=0;i<len;i++){
        largestArea = Math.max(largestArea,(right[i]-left[i]-1)*heights[i] );
    }
    return largestArea;
}

leetcode42接雨水(困难)

原题链接

/**
分析:每一根柱子能接的水都是由它左右两边  【最接近它的】 且 【比它高】 的柱子界定的.
创建一个单调栈,栈中元素都是单调递减的,遍历一遍height高度:
(1)当 当前高度小于前一个柱子的高度,那么说明当前柱子是被前一根柱子所界定的,此时开始有积水,直接入栈
(2)当 当前高度大于栈顶的柱子的高度,那么说明栈顶柱子是被当前柱子和其前面的柱子所界定的,栈顶的柱子出栈,比较该柱子前后两边界的高度,较小的决定了能接多少水,再计算两边界之间的距离,最后计算能够接到的水计入总结果res。
**/
public int trap(int[] height) {
    Deque<Integer> stack = new LinkedList<>();
    int current = 0;
    int distance = 0;
    int res = 0;
    while(current < height.length){
        while( !stack.isEmpty() && height[stack.peek()] < height[current] ){
            Integer h = height[stack.pop()];
            if(stack.isEmpty()) break;//栈空,没有左边界,无法蓄水
            distance = current - stack.peek() - 1;
            res += ( Math.min(height[current],height[stack.peek()]) - h ) * distance;
        }
        stack.push(current);
        ++current;
    }
    return res;
}

对于为什么要计算distance,例如下图
算法学习随记 - 单调栈_第1张图片
计算粉色框和红框是分开进行的,粉色框蓄水量的两根边界柱子距离distance为3。

leetcode739每日温度(中等)

原题链接
创建一个单调栈用于记录气温的大小,使栈中始终保持单调递减。

/**
     * 使用 “单调栈” 解决,可见这个单调栈是单调递减的
     * 遍历一次气温列表,
     *(1)当栈为空或者当前气温小于栈顶元素时,则直接入栈
     *(2)当 栈顶气温小于当前气温,则当前气温就是栈顶气温要观测到的那个更高的气温,直接出栈记录结果。重复第二步,最后再将当前气温入栈
     * 
     */
    public int[] dailyTemperatures(int[] T) {
        int len = T.length;
        int[] res = new int[len];
        LinkedList<Integer> stack = new LinkedList<>();
        for (int i=0;i<len;i++){
            while (!stack.isEmpty() && T[i]>T[stack.peek()]) {
                int preIndex = stack.pop();
                res[preIndex] = i - preIndex;
            }
            stack.push(i);
        }
        return res;
    }

leetcode316去除重复字符(中等)

原题链接
创建一个单调栈用于保持栈中元素的最小字典序

//字典序最小:字符串比较字典序时,是从头到尾依次比较相应位置上的字符的字典序大小的。
//创建一个“单调栈”(栈中的元素并不是完全的单调)用于保持栈中元素的最小字典序,步骤:
//(1)首先记录下每个字符在字符串中出现的最后一个位置;
//(2)若当前元素不存在,直接入栈,但是入栈前需要将【后面还会出现、且字典序大于当前元素的】栈顶元素都出栈;
//(3)若当前元素存在,不进行处理,因为前面已经维持好字典序了
public String removeDuplicateLetters(String s) {
    LinkedList<Character> stack = new LinkedList<>();
    HashSet<Character> set = new HashSet<>();//用于记录栈中的字符是否已经存入栈中
    HashMap<Character, Integer> last_occur = new HashMap<>();//记录下每个字符最后出现的位置
    for (int i=0;i<s.length();i++){
        last_occur.put(s.charAt(i),i);
    }
    for (int i=0;i<s.length();i++){
        char ch = s.charAt(i);
        if (!set.contains(ch)){//栈中不存在该值
            while(!stack.isEmpty() && stack.peek()>ch && last_occur.get(stack.peek())>i){
            	//移除字典序大于当前元素 且后面还会出现的栈顶元素,这是为了让字典序大的字符靠后一点,这样在比较字典序时就能更小。(字符串比较字典序时,是从头到尾依次比较相应位置上的字符的字典序大小的。)
                set.remove(stack.pop());
            }
            stack.push(ch);
            set.add(ch);
        }
    }
    //取出栈中的元素,得出结果
    StringBuilder sb = new StringBuilder(stack.size());
    Character[] chars = stack.toArray(new Character[stack.size()]);
    for (int i=stack.size()-1;i>=0;i--) sb.append(chars[i]);
    return sb.toString();
}

leetcode901股票价格跨度(中等)

原题链接

/**
 * 编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。
 * 今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。
 * 例如,如果未来7天股票的价格是 [100, 80, 60, 70, 60, 75, 85],那么股票跨度将是 [1, 1, 1, 2, 1, 4, 6]。
 *
 * 由于求的是【小于等于当前天数】的【最大连续天数】,可以通过创建一个单调递减栈,使用res数组记录每一天【小于等于当前天数】的【最大连续天数】。
 * 倘若有i和j两天的股票价格P[i]和P[j],且i
class StockSpanner {
	//单调递减栈
    private LinkedList<Integer> stack ;
    //结果栈,记录每天【小于等于当前天数】的【最大连续天数】
    private LinkedList<Integer> res_stack ;

    public StockSpanner() {
        stack = new LinkedList<>();
        res_stack = new LinkedList<>();
    }

    public int next(int price) {
        int count = 1;
        while( !stack.isEmpty() && stack.peek()<=price ){
            count += res_stack.pop();
            stack.pop();
        }
        stack.push(price);
        res_stack.push(count);
        return count;
    }
}

leetcode402移掉k位数字(中等)

原题链接

/**
 * 思路:从该字符串表示的整数的最高位开始,往后判断,令后面较大的数被较小的数所取代,结果就是较小的数,
 * 如"143",使用低位的3替换高位的4(相当于移除一个4),得到的结果就是较小的数
 * 因此可以使用一个单调递增栈,在移除k个数的过程中,栈中的元素是单调递增的。
 * 但是移除完k个数后,则是直接入栈的。
 */

public static String removeKdigits(String num, int k) {
    if( k<=0 ) return num;
    int len = num.length();
    LinkedList<Integer> stack = new LinkedList<>();
    for(int i=0;i<len;i++){
        int n  = num.charAt(i) - 48;
        while( !stack.isEmpty() && k>0 &&  n<stack.peek()){
            stack.pop();
            --k;
        }
        stack.push(n);
    }
    while( k!=0 ){
        stack.pop();
        --k;
    }
    StringBuilder sb = new StringBuilder();
    boolean leadingZero = true;//使用一个标记,令开头的0不加入结果中
    while (!stack.isEmpty()){
        Integer nm = stack.removeLast();
        if (leadingZero && nm == 0) continue;
        leadingZero = false;
        sb.append( nm );
    }
    return sb.length()==0?"0":sb.toString();
}

leetcode581最短无序连续子数组(中等)

原题链接

/**
 * 给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
 * 你找到的子数组应是最短的,请输出它的长度。
 */
public class _581 {
    /**
     * 利用单调栈解决问题思路:
     * 因为要让整个数组是升序排序的,可以创建一个单调递增栈,在入栈时保证栈中元素递增排序
     * 从左往右遍历数组,若当前元素大于栈顶元素直接入栈;若当前元素小于栈顶元素,则栈顶元素出栈,此时这个栈顶元素的位置就是当前元素原本应该在的位置(即从该位置为边界开始得子数组需要进行排序),循环该过程找到当前元素原本应在的位置。遍历后就可以找到需要排序的子数组的左边界。
     * 同理,从右往左遍历数组就可以找到子数组的右边界。
     * 最后相减即可得到结果。
     */
public static int findUnsortedSubarray(int[] nums) {
     if (nums==null) return 0;
     int left = nums.length;
     int right = -1;
     LinkedList<Integer> stack = new LinkedList<>();
     //找到左边界
     for (int i=0;i<nums.length;i++){
         while (!stack.isEmpty() && nums[i]<nums[stack.peek()]){
             Integer pop = stack.pop();
             if ( pop < left ) left = pop;
         }
         stack.push(i);
     }
     stack.clear();
     //找到右边界
     for (int i=nums.length-1;i>=0;i--){
         while (!stack.isEmpty() && nums[i]>nums[stack.peek()]){
             Integer pop = stack.pop();
             if ( pop > right ) right = pop;
         }
         stack.push(i);
     }
     return right-left>0 ? right-left+1 : 0;
 }
}

日后有做到其他单调栈的再继续补充,想要看到一道算法题可以马上想到相应的解法,还需要题量的积累和总结啊!

你可能感兴趣的:(数据结构和算法,数据结构,字符串,leetcode,算法,栈)