单调栈解决的问题
在数组中想找到一个数,左边和右边比这个数大/小、且离这个数最近的位置。
- 比如数组
[5,4,6,7,2,3]
中:5
:左边没有比它大的数,右边比它大的数为6
4
:左边比它大的数为5
,右边比它大的数为6
- 经典解法是遍历每个数,以这个数为中心向左右两边寻找,复杂度为 O ( N 2 ) O(N^2) O(N2)。
如果对每一个数都想求这样的信息,能不能整体代价达到$O(N) $?需要使用到单调栈结构
单调栈结构的原理和实现
准备一个无重复元素的数组,假如对于数组的数,我们要求的是左边和右边比这个数大、且离这个数最近的位置。
准备一个栈stack
,这个栈要保证从栈底到栈顶是由大到小的,栈中存放的是数字在数组中的下标
- 遍历数组,假设当前的数为
arr[i]
- 如果栈为空或
arr[i] < stack.peek()
,则将arr[i]
压入栈中(保证了栈的单调性) - 如果
arr[i] > stack.peek()
,这时候再压栈就会破坏单调性,所以需要弹出一些数据。当数据被弹出,它的信息(左右最大最近的数)就开始生成。假设a = stack.peek()
就是即将要弹出的数a
左边最近且比它大的数为:栈中a
后面的数(如果a
是栈底元素的话,就没有左最近最大的数)。a
右边最近且比它大的数为:即将要压入的数,即arr[i]
- 如果数组中的数已经遍历完,且栈不为空,则依次弹出栈中元素并生成信息,此时栈中弹出的所有的数都没有右边最近且比其大的数。
复杂度分析:每个元素只进栈一次、出栈一次,所以时间复杂度为 O ( N ) O(N) O(N)
现在来证明一下:
- 假设此时
c
要进栈且c>a
,a
要弹出且生成信息 - 为什么
a
的右最近且比其大的数就是c
?- 首先在数组中,
c
一定在a
的右边(因为我们是从左往右依次遍历的,a
先于c
进栈),a
和c
之间可能存在一些其他的值。 a
和c
之间是否可能存在一个数k
满足k>a
,即k
是a
的右最近且比其大的数?- 不可能,因为如果
k
存在的话,k
入栈的时候,a
就会被弹出。
- 不可能,因为如果
- 首先在数组中,
- 为什么
a
的左最近且比其大的数就是b
?- 栈是单调的,所以一定存在
b > a
, b
和a
之间是否存在一个数k
满足k>a
,即k
是a
的左最近且比其大的数?- 不可能,因为如果存在的话,放在
a
后面的就是不是b
而是k
。
- 不可能,因为如果存在的话,放在
- 栈是单调的,所以一定存在
上面说的是数组无重复值的情况,那是否可以解决数组有重复值的情况?
- 重复值放在一起即可,比如栈中放入链表,每个链表上的节点都是具有重复的值的下标。
JavaCode:
public class GetLeftAndRightMax {
/**
* @param arr 要求的数组
* @return 长度与arr相同
* · res[i][0]表示左边比这个数大,且离这个数最近的位置
* · res[i][1]表示右边比这个数大,且离这个数最近的位置
*/
public static int[][] getLeftAndRightMax(int[] arr) {
int[][] res = new int[arr.length][2];
Stack<ArrayList<Integer>> stack = new Stack<>();
for (int i = 0; i < arr.length; i++) {
// 弹出栈中小于arr[i]的元素,并生成res中的信息
while (!stack.isEmpty() && arr[stack.peek().get(0)] < arr[i]) {
ArrayList<Integer> cur = stack.pop();
for (Integer index : cur) {
res[index][0] = stack.isEmpty()?-1:stack.peek().get(0);
res[index][1] = i;
}
}
// 向栈中压入arr[i]
if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
// 栈顶元素的值与arr[i]相等
stack.peek().add(i);
} else {
// 栈顶元素的值与arr[i]不相等
ArrayList<Integer> tmp = new ArrayList<>();
tmp.add(i);
stack.push(tmp);
}
}
// 把栈中剩余的元素弹出并生成信息
while (!stack.isEmpty()) {
ArrayList<Integer> cur = stack.pop();
for (Integer index : cur) {
res[index][0] = stack.isEmpty()?-1:stack.peek().get(0);
res[index][1] = -1;
}
}
return res;
}
public static void main(String[] args) {
System.out.println(Arrays.deepToString(getLeftAndRightMax(new int[]{5, 4, 6, 5, 2, 3}))); // [[-1, 2], [0, 2], [-1, -1], [2, -1], [3, 5], [3, -1]]
}
}
单调栈应用
【题目描述】:
- 定义:数组中累积和与最小值的乘积,假设叫做指标A。
- 给定一个数组,该数组元素均为正数,请返回子数组中,指标A最大的值。
【解题思路】:
- 遍历数组中的元素,将遍历到的元素
arr[i]
看做是子数组中的最小值,则指标A最大时,就是子数组元素个数最多时。 - 利用单调栈,找到
arr[i]
的左边和右边最近且比它小的数,这两个数就是arr[i]
对应子数组的左右边界(不包括),计算出子数组的指标A - 返回指标A最大的值
class Solution {
public static int getMaxA(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
Stack<ArrayList<Integer>> stack = new Stack<>();
int[][] arrSec = new int[arr.length][2];
for (int i = 0; i < arr.length; i++) {
while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
List<Integer> list = stack.pop();
for (Integer integer : list) {
arrSec[integer][0] = stack.isEmpty() ? 0 : stack.peek().get(0) + 1;
arrSec[integer][1] = i - 1;
}
}
if (!stack.isEmpty() && arr[i] == arr[stack.peek().get(0)]) {
stack.peek().add(i);
} else {
ArrayList<Integer> list = new ArrayList<>();
list.add(i);
stack.push(list);
}
}
while (!stack.isEmpty()) {
List<Integer> list = stack.pop();
for (Integer integer : list) {
arrSec[integer][0] = stack.isEmpty() ? 0 : stack.peek().get(0) + 1;
arrSec[integer][1] = arr.length - 1;
}
}
int max = -1;
for (int i = 0; i < arrSec.length; i++) {
int sum = 0;
for (int j = arrSec[i][0]; j <= arrSec[i][1]; j++) {
sum += arr[j];
}
max = Math.max(max, sum * arr[i]);
}
return max;
}
public static void main(String[] args) {
System.out.println(getMaxA(new int[]{3, 4, 4, 1}));
}
}