LeetCode 16 - 单调栈

单调栈还是一个栈,只是要求每次入栈的元素必须有序——如果新元素入栈时不符合要求,就将之前的元素出栈,直到符合要求才入栈,形成一个 单调递增/减 的栈。(递增递减是根据出栈序列来说的,即从栈顶到栈底)

  • 单调递增:只有比栈顶小的才能入栈,适用于求解 第一个大于该位置元素的数。
  • 单调递减:只有比栈顶大的才能入栈,适用于求解 第一个小于该位置元素的数。

代码模板:

Deque<Integer> stack = new LinkedList<>();
for(int i = 0; i < nums.length; i++) {
    // 递减栈
    while(!stack.isEmpty() && stack.peek() > nums[i])
        stack.pop();
    stack.push(nums[i]);
}

84. 柱状图中最大的矩形#

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够 勾勒 出来的矩形的最大面积。

可以考虑枚举矩形的宽和高:

  • 如果 枚举宽,可以使用两重循环枚举矩形的 左右边界 以确定这个宽,此时矩形的高度是在左右边界之内的柱子的 最小高度

    int largestRectangleArea(int[] heights) {
    	int len = heights.length;
    	int area = 0;
    	// 枚举左边界
    	for(int left = 0; left < len; left++) {
            int minHeight = heights[left];
            // 枚举右边界
            for(int right = left; right < n; right++) {
                minHeight = Math.min(minHeight, heights[right]);
                area = Math.max(area, (right-left+1) * minHeight); 
            }
        }
        return area;
    }
    
  • 如果 枚举高,可以使用一层循环枚举某一根柱子,将其固定为矩形的高 h,然后从这根柱子开始 向两侧延伸,直到 遇到高度小于 h 的柱子,就确定了矩形的左右边界。

    int largestReactangleArea(int[] heights) {
        int len = heights.length;
        int area = 0;
        // 枚举每一根柱子
        for(int mid = 0; mid < n; mid++) {
            int height = heights[mid];
            // 向两边延伸寻找左右边界
            int left = mid, right = mid;
            // 注意这里的 left-1, right+1
            while(left > 0 && heights[left-1] >= height) 
                left--;
            while(right < n && heights[right+1] >= height) 
                right++;
            // 此时 left, right 即为左右边界
            area = Math.max(area, (right-left+1) * height);
        }
        return area;
    }
    

这两种暴力方法的时间复杂度都为 O(n2),需要进行优化,考虑到枚举高的方法使用了一层循环,我们来优化这种方法。

方法:单调栈

思路:对每一个高度,可以求得左右边界,继而求出对应的面积;那么遍历所有高度,即可求出最大面积。使用单调栈,在出栈操作时得到前后边界并计算面积

  • 栈中存放了 j 值,即某些柱子对应的 索引,它们对应的高度值单调递增
  • 枚举到第 i 根柱子时,从栈顶不断移除所有对应高度值大于等于当前高度 heights[i] 的索引。移除完毕后,栈顶的索引值 j 就是 i 左侧距离最近的小于其高度的柱子。
    • 这里有一种 特殊情况,如果移除后栈变为空,说明左侧所有柱子都不低于当前柱子,那么可以认为这个 j 值为 -1 。(这个虚拟的柱子称为 哨兵
  • 移除完成后,将当前柱子的索引 i 放入栈顶。

上面的步骤可以求出各个柱子的左边界,用相同的方法,但是 从右向左遍历,就可以得到各个柱子的 右边界

int largestRectangleArea(int[] heights) {
  int len = heights.length;
  int[] left = new int[len];
  int[] right = new int[len];
  Deque<Integer> stack = new LinkedList<>();
  // 填充 left 数组(各个柱子的左边界)
  for (int i = 0; i < len; i++) {
    // 找到栈中第一个小于当前高度的柱子
    while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) stack.pop();
    // left[i] 表示柱子 i 的高度确定的左边界
    left[i] = stack.isEmpty() ? -1 : stack.peek();
    stack.push(i); // 将当前索引入栈
  }

  // 填充右边界数组
  stack.clear();
  for (int i = n - 1; i >= 0; i--) {
    while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) stack.pop();
    right[i] = stack.isEmpty() ? n : stack.peek();
    stack.push(i);
  }

  int area = 0;
  for (int i = 0; i < n; i++)
    area = Math.max(area, (right[i] - left[i] - 1) * heights[i]);
  return area;
}

优化:上面的方法在确定左右边界需要分别进行一次遍历,其实可以进行优化,只使用一次遍历就可以确定两个边界。因为 在对索引 i 进行出栈时,可以确定它的右边界。当索引 i 出栈时,说明当前正在处理的索引 i0 对应柱子的高度 小于等于 i 的高度,所以索引 i0 就是柱子 i 的右边界。

int largestRectangleArea(int[] heights) {
    int n = heights.length;
    int[] left = new int[n];
    int[] right = new int[n];
    Arrays.fill(right, n); // 注意对 right 的初始化
    Deque<Integer> stack = new LinkedList<>();
    for(int i = 0; i < n; i++) {
        // 确定左边界的方法不变
        while(!stack.isEmpty() 
              && heights[stack.peek()] >= heights[i]) {
            right[stack.peek()] = i; // 在出栈时确定栈顶元素右边界
            stack.pop();
        }
        left[i] = stack.isEmpty() ? -1 : stack.peek();
        stack.push(i);
    }
    int area = 0;
    for(int i = 0; i < n; i++)
        area = Math.max(area, (right[i]-left[i]-1) * heights[i]);
    return area;
}

85. 最大矩形#

给定一个仅包含 0 和 1 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。

方法一:优化暴力解法

首先计算每个元素的 左边(包括自身)连续 1 的数量,使用 left[i][j] 记录。

随后,我们枚举 以该点为右下角的全 1 矩形:

  1. 首先看当前行,因为高度为 1,所以全 1 矩形面积为 left[i][j] * 1
  2. 然后向上扩展一行,矩形高度变成 2,宽度变成了 min(left[i][j], left[i-1][j])
  3. 以此类推,直到扩展到第一行。

对每个点重复这个过程,就能得到所有矩形里面的最大矩形。

我们 预计算最大宽度 的方法实际上将输入转换成了一系列柱状图,针对柱状图计算最大面积。

int maxRectangle(char[][] matrix) {
    int rows = matrix.length;
    if(rows == 0) return 0;
    int cols = matrix[0].length;
    // 计算 left 数组
    int[][] left = new int[rows][cols];
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            if(matrix[i][j] == '1')
                left[i][j] = 1 + (j == 0 ? 0 : left[i][j-1]);
        }
    }
    int result = 0;
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            if(matrix[i][j] == '0') continue;
            int width = left[i][j];
            int area = width;
            for(int k = i-1; k >= 0; k--) {
                width = Math.min(width, left[k][j]);
                area = Math.max(area, (i-k+1) * width);
            }
            result = Math.max(result, area);
        }
    }
    return result;	
}

方法二:单调栈

在方法一中,将输入拆分成了 一系列柱状图,为了计算最大矩形,只需要计算 每个柱状图 中的最大面积,并找到全局最大值即可。

和 84 题实际上相同,只是柱状图旋转了 90 度,所以原来的为每一根柱子寻找左右边界,变成了为每一根(横着放置)的柱子寻找上下边界。

int maxRectangle(char[][] matrix) {
    int rows = matrix.length, cols = matrix[0].length;
    // 填充 left 数组(构成一系列柱状图)
    int[][] left = new int[rows][cols];
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            if(matrix[i][j] == '1')
                left[i][j] = 1 + (j == 0 ? 0 : left[i][j-1]);
        }
    }
    
    int maxArea = 0;
    // 枚举每一列,计算以该列为底线的柱状图
    for(int j = 0; j < cols; j++) {
        // 因为柱状图旋转了,所以左右边界变成了上下边界
        int[] top = new int[rows];
        int[] bottom = new int[rows];
        Deque<Integer> stack = new LinkedList<>();
        // 从第一行开始枚举柱子的「高」,寻找上边界
        for(int i = 0; i < rows; i++) {
            while(!stack.isEmpty() 
                  && left[stack.peek()][j] >= left[i][j])
                stack.pop();
            top[i] = stack.isEmpty() ? -1 : stack.peek();
            stack.push(i);
        }
        // 从最后一行开始枚举柱子的高,寻找下边界
        stack.clear();
        for(int i = rows-1; i >= 0; i--) {
            while(!stack.isEmpty()
                  && left[stack.peek()][j] >= left[i][j])
                stack.pop();
                bottom[i] = stack.isEmpty() ? rows : stack.peek();
            stack.push(i);
        }
        
        for(int i = 0; i < rows; i++) {
            int height = bottom[i] - top[i] - 1;
            int area = height * left[i][j];
            maxArea = Math.max(area, maxArea);
        }
    }
    return maxArea;
}

仿照 84 题,也可以将这里的填充 top, bottom 数组操作利用一次循环完成:

int[] top = new int[rows];
int[] bottom = new int[rows];
Arrays.fill(bottom, rows);
for(int i = 0; i < rows; i++) {
    while(!stack.isEmpty() 
          && left[stack.peek()][j] >= left[i][j]) {
        bottom[stack.peek()] = i;
        stack.pop();
    }
    top[i] = stack.isEmpty() ? -1 : stack.peek();
    stack.push(i);
}

503. 下一个更大元素 II#

给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素

数字 x 的 下一个更大的元素是 按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。

方法:单调栈

递增递减的定义是根据从 栈顶 → 栈底 的顺序来说的:

  • 单调 递增栈最小元素位于栈顶,用于求解 第一个大于当前位置元素的数;(假设当前位置为 i ,那么栈顶元素如果小于 nums[i] ,那么会被弹出,弹出的同时 它们也找到了 下一个更大元素,就是 i 处的元素)
  • 单调 递减栈最大元素位于栈顶,用于求解 第一个小于当前位置元素的数。

本问题要求出下一个更大元素,可以利用 递增栈。每枚举到一个元素位置 i ,首先将栈中所有小于等于 nums[i] 的元素出栈,这些 被弹出元素的下一个更大的元素 就是 nums[i]。然后将 i 入栈。

但因为是循环数组,所以只遍历一遍还不行,例如 [2, 3, 1] 遍历完成后栈中元素为 [3, 1] ,1 的下一个更大元素还不知道。这可以通过 扩展下标遍历范围2*n对下标取模 来处理。

int nextGreaterElement(int[] nums) {
  int len = nums.length;
  int[] result = new int[len];
  Arrays.fill(result, -1); // 如果没有更大的就记为 -1
  Deque<Integer> stack = new LinkedList<>();
  for(int i = 0; i < 2 * len; i++) {
    while(!stack.isEmpty() && nums[stack.peek()] < nums[i % len]) {
      // 此时栈顶元素的下一个更大元素就是 nums[i]
      result[stack.pop()] = nums[i % len];
    }
    // 将索引入栈
    stack.push(i % len);
  }
  return result;
}

739. 每日温度#

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指在第 i 天之后,才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。

方法:单调栈

这个问题和 503 题(求解下一个更大元素)很相似,但是更简单;不同之处在于,要求解的不是下一个更大元素,而是下一个更大元素与当前元素的距离

public int[] dailyTemperatures(int[] temperatures) {
    int len = temperatures.length;
    int[] result = new int[len];
    Arrays.fill(result, 0);
    Deque<Integer> stack = new LinkedList<>();
    for(int i = 0; i < len; i++) {
        while(!stack.isEmpty() 
              && temperatures[stack.peek()] < temperatures[i]) {
            result[stack.peek()] = i - stack.peek();
            stack.pop();
        }
        stack.push(i);
    }
    return result;
}
posted @   李志航  阅读(64)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示
主题色彩