文章目录
数据结构与算法(四)
25 单调栈
- 内容:
- 单调栈的原理(无重复数+有重复数)
- 用题目来学习单调栈提供的便利性
25.1 单调栈实现(无重复数+有重复数)
- 方法一:arr数组中无重复值
// arr = [ 3, 1, 2, 3]
// 0 1 2 3
// [
// 0 : [-1, 1]
// 1 : [-1, -1]
// 2 : [ 1, -1]
// 3 : [ 2, -1]
// ]
public static int[][] getNearLessNoRepeat(int[] arr) {
int[][] res = new int[arr.length][2];
// 只存位置,位置所代表的值(底->顶)从小到大
Stack<Integer> stack = new Stack<>();
// 依次遍历到i位置的数,arr[i]
for (int i = 0; i < arr.length; i++) {
// 栈不空 且 栈顶的数大于此时的arr[i],则栈顶需要弹出,并整理答案了
while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) {
int j = stack.pop();
res[j][0] = stack.isEmpty() ? -1 : stack.peek();
res[j][1] = i;
}
stack.push(i);
}
// 最后栈不空,单独收集答案
while (!stack.isEmpty()) {
int j = stack.pop();
res[j][0] = stack.isEmpty() ? -1 : stack.peek();
res[j][1] = -1;
}
return res;
}
- 方法二:arr数组中有重复值
public static int[][] getNearLess(int[] arr) {
int[][] res = new int[arr.length][2];
Stack<List<Integer>> stack = new Stack<>();
for (int i = 0; i < arr.length; i++) {
// i -> arr[i] 进栈
while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
List<Integer> items = stack.pop();
int leftLessIdx = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
for (Integer x : items) {
res[x][0] = leftLessIdx;
res[x][1] = i;
}
}
if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
stack.peek().add(i);
} else {
List<Integer> list = new ArrayList<>();
list.add(i);
stack.push(list);
}
}
// 最后栈不空,单独收集答案
while (!stack.isEmpty()) {
List<Integer> items = stack.pop();
int leftLessIdx = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
for (Integer x : items) {
res[x][0] = leftLessIdx;
res[x][1] = -1;
}
}
return res;
}
25.2 子数组累加和乘以子数组最小值中的最大值
- 给定一个只包含正数的数组arr,arr中任何一个子数组sub,一定都可以算出 A指标:(sub累加和 )*(sub中的最小值)是什么,那么所有子数组中,这个值最大是多少?
- 方法一:暴力枚举每一个子数组,计算指标A(子数组累加和*子数组最小值)
public static int maxIndicatorA0(int[] arr) {
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
for (int j = i; j < arr.length; j++) {
int sum = 0, minNum = Integer.MAX_VALUE;
for (int k = i; k <= j; k++) {
sum += arr[k];
minNum = Math.min(minNum, arr[k]);
}
max = Math.max(max, sum * minNum);
}
}
return max;
}
- 方法二:单调栈+前缀和
public static int maxIndicatorA(int[] arr) {
int n = arr.length, max = Integer.MIN_VALUE;
int[] sums = new int[n + 1];
for (int i = 0; i < n; i++) sums[i + 1] = sums[i] + arr[i];
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < n; i++) {
// >=: 可能存在重复值,会导致计算错误,但是最后一个相等值会计算正确
while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
int j = stack.pop();
max = Math.max(max, arr[j] * (stack.isEmpty() ? sums[i] : (sums[i] - sums[stack.peek() + 1])));
}
stack.push(i);
}
while (!stack.isEmpty()) {
int j = stack.pop();
max = Math.max(max, arr[j] * (stack.isEmpty() ? sums[n] : (sums[n] - sums[stack.peek() + 1])));
}
return max;
}
25.3 柱状图中最大的矩形
- 给定一个非负数组arr,代表直方图,返回直方图的最大长方形面积。
- 测试链接:https://leetcode.cn/problems/largest-rectangle-in-histogram
【思路分析】利用单调栈结构,可以很方便得到 每一个高度 位置j离它最近且小于它的高度的 左右两边的位置(l, r),面积=heights[j]*(r-1-l)
- 方法一:利用单调栈结构,依次遍历每一个高度,计算基于每一个高度所能组成的矩形面积,求最大值
public static int largestRectangleArea(int[] heights) {
if (heights == null || heights.length == 0) return 0;
int maxArea = 0;
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < heights.length; i++) {
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
int j = stack.pop(), left = stack.isEmpty() ? -1 : stack.peek();
maxArea = Math.max(maxArea, heights[j] * (i - 1 - left));
}
stack.push(i);
}
while (!stack.isEmpty()) {
int j = stack.pop(), left = stack.isEmpty() ? -1 : stack.peek();
maxArea = Math.max(maxArea, heights[j] * (heights.length - 1 - left));
}
return maxArea;
}
- 方法二:基于数组实现单调栈
public static int largestRectangleArea1(int[] heights) {
if (heights == null || heights.length == 0) return 0;
// si用于数组栈元素访问,-1: 表示栈空
int n = heights.length, maxArea = 0, si = -1;
int[] stack = new int[n];
for (int i = 0; i < n; i++) {
while (si != -1 && heights[stack[si]] >= heights[i]) {
int j = stack[si--], k = si == -1 ? -1 : stack[si];
maxArea = Math.max(maxArea, heights[j] * (i - k - 1));
}
stack[++si] = i;
}
while (si != -1) {
int j = stack[si--], k = si == -1 ? -1 : stack[si];
maxArea = Math.max(maxArea, heights[j] * (n - k - 1));
}
return maxArea;
}
- 方法三:单调栈编码实现不一样
public int largestRectangleArea2(int[] heights) {
int n = heights.length, maxArea = 0;
// arr记录每个位置i,离它最近且小于arr[i]的左右位置
int[][] arr = new int[n][2];
for (int i = 0; i < arr.length; i++) {
arr[i][0] = -1;
arr[i][1] = n;
}
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
arr[stack.pop()][1] = i;
}
if (!stack.isEmpty()) arr[i][0] = stack.peek();
stack.push(i);
}
for (int i = 0; i < n; i++) {
maxArea = Math.max(maxArea, heights[i] * (arr[i][1] - arr[i][0] - 1));
}
return maxArea;
}
25.4 最大矩形
- 给定一个二维数组matrix,其中的值不是0就是1,返回全部由1组成的最大子矩形内部有多少个1(面积)。
- 测试链接:https://leetcode.cn/problems/maximal-rectangle/
【思路分析】对二维数组进行压缩,遍历每一行,转化成以当前行作为地基时的高度数组(遇到0则高度0,否则上一行高度+1),求解每一行高度数组直方图的最大面积。
[1, 0, 1, 1, 1] [1, 0, 1, 1, 1]
[0, 1, 0, 1, 0] [0, 1, 0, 2, 0]
[1, 1, 0, 1, 1] ==>> 转化成: [1, 2, 0, 3, 1]
[1, 1, 0, 1, 1] [2, 3, 0, 4, 2]
[0, 1, 1, 1, 1] [0, 4, 1, 5, 3]
public static int maximalRectangle(char[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return 0;
int ans = 0, m = matrix.length, n = matrix[0].length;
int[] heights = new int[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
heights[j] = matrix[i][j] == '0' ? 0 : heights[j] + 1;
}
ans = Math.max(ans, rectangleMaxArea(heights));
}
return ans;
}
// heights 是直方图数组
private static int rectangleMaxArea(int[] heights) {
int maxArea = 0, n = heights.length;
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
int j = stack.pop(), k = stack.isEmpty() ? -1 : stack.peek();
maxArea = Math.max(maxArea, heights[j] * (i - k - 1));
}
stack.push(i);
}
while (!stack.isEmpty()) {
int j = stack.pop(), k = stack.isEmpty() ? -1 : stack.peek();
maxArea = Math.max(maxArea, heights[j] * (n - k - 1));
}
return maxArea;
}
25.5 统计全1子矩形
- 给定一个二维数组matrix,其中的值不是0就是1,返回全部由1组成的子矩形数量。
- 测试链接:https://leetcode.cn/problems/count-submatrices-with-all-ones
【思路分析】对二维数组进行压缩,遍历每一行,转化成以当前行作为地基时的高度数组(遇到0则高度0,否则上一行高度+1),转化成求解每个直方图数组heights中的子矩形数量,最后累加所有子矩阵数量。
计算 heights[i] 的子矩形数量:假设左边到不了的位置是 X,右边到不了的位置是 Y,可以到达的区域长度 L,那么子矩形数量 = (L*(L+1)/2) * (heights[i] - max(X,Y))
[1, 0, 1, 1, 1] [1, 0, 1, 1, 1]
[0, 1, 0, 1, 0] [0, 1, 0, 2, 0]
[1, 1, 0, 1, 1] ==>> 转化成: [1, 2, 0, 3, 1]
[1, 1, 0, 1, 1] [2, 3, 0, 4, 2]
[0, 1, 1, 1, 1] [0, 4, 1, 5, 3]// 比如
// 1
// 1
// 1 1
// 1 1 1
// 1 1 1
// 1 1 1
//
// 2 … 6 … 9
// 如上图,假设在6位置,1的高度为6
// 在6位置的左边,离6位置最近、且小于高度6的位置是2,2位置的高度是3
// 在6位置的右边,离6位置最近、且小于高度6的位置是9,9位置的高度是4
// 此时我们求什么?
// 1) 求在3~8范围上,必须以高度6作为高的矩形,有几个?
// 2) 求在3~8范围上,必须以高度5作为高的矩形,有几个?
// 也就是说,<=4的高度,一律不求
// 那么,1) 求必须以位置6的高度6作为高的矩形,有几个?
// 3…3 3…4 3…5 3…6 3…7 3…8
// 4…4 4…5 4…6 4…7 4…8
// 5…5 5…6 5…7 5…8
// 6…6 6…7 6…8
// 7…7 7…8
// 8…8
// 这么多!= 21 = (9 - 2 - 1) * (9 - 2) / 2
// 这就是任何一个数字从栈里弹出的时候,计算矩形数量的方式
public static int numSubmat(int[][] mat) {
if (mat == null || mat.length == 0 || mat[0].length == 0) return 0;
int ans = 0, m = mat.length, n = mat[0].length;
int[] heights = new int[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
heights[j] = mat[i][j] == 0 ? 0 : heights[j] + 1;
}
ans += countFromBottom(heights);
}
return ans;
}
private static int countFromBottom(int[] heights) {
int cnt = 0, n = heights.length, si = -1;
// 自定义数组模拟栈
int[] stack = new int[n];
for (int i = 0; i < n; i++) {
while (si != -1 && heights[stack[si]] >= heights[i]) {
int j = stack[si--];
// 考虑重复情况,必须严格大于是才结算,最后一个重复值会算对
if (heights[j] > heights[i]) {
int k = si == -1 ? -1 : stack[si], m = i - k - 1;
int minmax = Math.max(k == -1 ? 0 : heights[k], heights[i]);
cnt += (heights[j] - minmax) * num(m);
}
}
stack[++si] = i;
}
while (si != -1) {
int j = stack[si--];
int k = si == -1 ? -1 : stack[si], m = n - k - 1;
int minmax = k == -1 ? 0 : heights[k];
cnt += (heights[j] - minmax) * num(m);
}
return cnt;
}
private static int num(int n) {
return ((n * (1 + n)) >> 1);
}
26 单调栈相关的题目(续)、斐波那契数列的矩阵快速幂模型
- 内容:
- 再讲一个单调栈相关的面试题
- 斐波那契数列的矩阵快速幂模型详解
26.1 子数组的最小值之和
- 给定一个数组arr,返回所有子数组最小值的累加和。
- 方法一:暴力解
public static int subArrayMinSum1(int[] arr) {
int n = arr.length, ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
// 枚举每一个子数组,求子数组的最小值
int min = arr[i];
for (int k = i + 1; k <= j; k++) {
min