数据结构与算法(四)

数据结构与算法(四)

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
  • 15
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值