1. 需求
[问题]
给定一个不含重复值数组的arr
,找到每一个i
位置左边和右边离i
位置最近且比arr[i]
小的位置,时间复杂度为
O
(
N
)
O(N)
O(N)
2. 方法论
准备一个栈stack
,栈中元素放数组位置,开始stack
为空。
- 如果找到每个
i
位置左边和右边离i
位置最近且值比arr[i]
小的位置,那么需要让stack
从栈顶到栈底的位置所代表的值是严格递减的 - 如果找到每个
i
位置左边和右边离i
位置最近且值比arr[i]
大的位置,那么需要让stack
从栈顶到栈底的位置所代表的值是严格递增的
典例演示1
初始数组
a
r
r
=
{
3
,
4
,
1
,
5
,
6
,
2
,
7
}
arr=\{3, 4, 1, 5, 6, 2, 7\}
arr={3,4,1,5,6,2,7},stack
从栈顶到栈底为
{
}
\{\}
{}
遍历到
a
r
r
[
0
]
=
3
arr[0]=3
arr[0]=3,stack
空,直接放入0
位置,stack
从栈顶到栈底为
{
0
位置(值
3
)
}
\{0位置(值3)\}
{0位置(值3)}
遍历到
a
r
r
[
1
]
=
4
arr[1]=4
arr[1]=4,直接放入1
位置,不会破坏从栈顶到栈底位置所代表的值的递减顺序,stack
从栈顶到栈底为
{
1
位置
,
0
位置
}
\{1位置, 0位置\}
{1位置,0位置}
遍历到
a
r
r
[
2
]
=
1
arr[2]=1
arr[2]=1,直接放入2
位置会破坏从栈顶到栈底位置所代表的值的递减顺序,所以从stack
开始弹出位置,若x
位置被弹出,在栈中位于x
位置下面的位置,就是x
位置左边离x
最近且值比arr[x]
小的位置,
当前遍历到的位置是x
位置右边离x
位置最近且比arr[x]
小的位置,从stack
弹出位置在栈中位于1
位置下面的是位置0
,当前遍历的位置2
,所以
a
n
s
[
1
]
=
{
0
,
2
}
ans[1]=\{0,2\}
ans[1]={0,2}
弹出位置1
后,发现放入2
位置还会破坏从栈顶到栈底位置所代表的值的递减顺序,所以继续弹出位置0
在栈中位于0
位置下面没有位置,说明位置0
左边不存在比arr[0]
小的值,当前遍历的位置是2
,所以
a
n
s
[
0
]
=
{
−
1
,
2
}
ans[0]=\{-1, 2\}
ans[0]={−1,2}
stack
已为空,所以放入2
位置,stack
从栈顶到栈底为
{
2
位置
}
\{2位置\}
{2位置}
遍历到
a
r
r
[
3
]
=
5
arr[3]=5
arr[3]=5,直接放入3
位置,不会破坏从栈顶到栈底位置所代表的值的递减顺序,stack
从栈顶到栈底为
{
3
位置
,
2
位置
}
\{3位置, 2位置\}
{3位置,2位置}
遍历到
a
r
r
[
4
]
=
6
arr[4]=6
arr[4]=6,直接放入4
位置,不会破坏从栈顶到栈底位置所代表的值的递减顺序,stack从栈顶到栈底为
{
4
位置
,
3
位置
,
2
位置
}
\{4位置, 3位置, 2位置\}
{4位置,3位置,2位置}
遍历到
a
r
r
[
5
]
=
2
arr[5]=2
arr[5]=2,直接放入2
位置,会破坏从栈顶到栈底位置所代表的值的递减顺序,开始弹出位置,弹出位置4
,栈中它的下面位置是3
,当前位置是5
,
a
n
s
[
4
]
=
3
,
5
ans[4]={3,5}
ans[4]=3,5
弹出位置3
,栈中它的下面位置是2
,当前位置是5
,
a
n
s
[
3
]
=
2
,
5
ans[3]={2, 5}
ans[3]=2,5
放入5
位置不会破坏单调性
遍历到
a
r
r
[
6
]
=
7
arr[6]=7
arr[6]=7,发现直接放入6
位置,不会破坏单调性,那么直接放入
遍历结束后清算剩下位置
弹出6
位置,栈中下面是位置5
,
a
n
s
[
6
]
=
5
,
−
1
ans[6]={5, -1}
ans[6]=5,−1
弹出5
位置,栈中下面是位置2
,
a
n
s
[
5
]
=
2
,
−
1
ans[5]={2, -1}
ans[5]=2,−1
弹出2
位置,栈中下面没有位置信息,
a
n
s
[
2
]
=
−
1
,
−
1
ans[2]={-1, -1}
ans[2]=−1,−1
算法证明
证明:在单调栈中,
如果x
位置被弹出,在栈中位于x
位置下面的位置是x
位置左边离x
位置最近且值比arr[x]
小的位置,
当遍历到的位置就是x
位置右边离x
位置最近且值比arr[x]小
的位置
假设数组中元素不重复
- 当前来到
j
位置,x
位置已经在栈中,所以x
位置一定在j
位置左边:…5(x
位置)…4(j
位置)…。如果5和4之间存在小于5的数,那么没等到遍历j
位置,x
位置已经被弹出了,轮不到当前j位置让x
位置弹出,所以5和4之间要么没有数,要么比5大,因此x
位置右边离x
最近的位置且值小于arr[x]
的位置是j
位置 - 当前弹出的位置是
x
位置,x
位置下面是位置i
,i
比x
位置早进栈,所以i
位置肯定在x
位置左边:…1(i
位置)…5(x
位置)…。如果1和5之间存在小于1的数,那么i
位置会被提前弹出,在栈中i
位置和x
位置不可能在一起。如果1和5之间存在大于1小于5的数,那么栈中i
位置和x
位置之间会夹上一个别的位置,也不可能贴在一起,所以1和5之间要么没有,要么一定比5大,那么x
位置左边离x
位置最近且小于arr[x]
的位置就是i
位置
时间复杂度
整个流程中,每个位置进栈一次、出栈一次,时间复杂度为 O ( N ) O(N) O(N)
算法实现
/**
* 数组无重复情况
* @param arr
* @return
*/
public int[][] getNearLessNoRepeat(int[] arr) {
int[][] res = new int[arr.length][2];
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < arr.length; i++) {
// 出现破坏从栈顶到栈底值递减单调性的情况
while (stack.isEmpty() && arr[stack.peek()] > arr[i]) {
int popIndex = stack.pop();
// popIndex位置左边离popIndex最近且值比它小的位置
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
res[popIndex][0] = leftLessIndex;
// popIndex位置右边离popIndex最近且值比它小的位置
res[popIndex][1] = i;
}
// 没有破坏单调性就压栈
stack.push(i);
}
// 元素遍历结束,处理栈中剩余元素
while (!stack.isEmpty()) {
int popIndex = stack.pop();
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
// popIndex位置左边离popIndex最近且值比它小的位置
res[popIndex][0] = leftLessIndex;
// popIndex位置右边离popIndex最近且值比它小的位置
res[popIndex][1] = -1;
}
return res;
}
进阶问题
给定一个可能含有重复值的数组arr
,找到每一个i
位置左边和右边离i
位置最近且值比arr[i]
小的位置
典例演示2
初始数组
a
r
r
=
{
3
,
1
,
3
,
4
,
3
,
5
,
3
,
2
,
2
}
arr=\{3, 1, 3, 4, 3, 5, 3, 2, 2\}
arr={3,1,3,4,3,5,3,2,2},stack
从栈顶到栈底为
{
}
\{\}
{}
遍历到
a
r
r
[
0
]
=
3
arr[0]=3
arr[0]=3,发现栈空,直接放入0
位置。stack
从栈顶到栈底
0
位置
(
3
)
{0位置(3)}
0位置(3)
遍历到
a
r
r
[
1
]
=
1
arr[1]=1
arr[1]=1,从栈中弹出0
位置,
a
n
s
[
0
]
=
{
−
1
,
1
}
ans[0]=\{-1,1\}
ans[0]={−1,1},位置1
进栈。stack
从栈顶到栈底
{
1
位置
(
1
)
}
\{1位置(1)\}
{1位置(1)}
遍历到
a
r
r
[
2
]
=
3
arr[2]=3
arr[2]=3,发现2
位置可以直接放入,位置2
进栈。stack
从栈顶到栈底
{
2
位置
(
3
)
,
1
位置
(
1
)
}
\{2位置(3), 1位置(1)\}
{2位置(3),1位置(1)}
遍历到
a
r
r
[
3
]
=
4
arr[3]=4
arr[3]=4,发现arr[3]
可以直接放入,位置3
进栈。stack
从栈顶到栈底
{
3
位置
(
4
)
,
2
位置
(
3
)
,
1
位置
(
1
)
}
\{3位置(4), 2位置(3), 1位置(1)\}
{3位置(4),2位置(3),1位置(1)}
遍历到
a
r
r
[
4
]
=
3
arr[4]=3
arr[4]=3,发现arr[4]
不能直接放入,弹出3
位置,
a
n
s
[
3
]
=
{
2
,
4
}
ans[3]=\{2,4\}
ans[3]={2,4}。此时发现栈顶是位置2
,值是3,当前遍历位置是4
,值也是3,两个位置压在一起。stack
从栈顶到栈底
{
∣
2
位置
,
4
位置
∣
(
3
)
,
1
位置
(
1
)
}
\{|2位置,4位置|(3), 1位置(1)\}
{∣2位置,4位置∣(3),1位置(1)}
遍历到
a
r
r
[
5
]
=
5
arr[5]=5
arr[5]=5,发现5
位置直接放入。stack
从栈顶到栈底
{
5
位置
(
5
)
,
∣
2
位置
,
4
位置
∣
(
3
)
,
1
位置
(
1
)
}
\{5位置(5) ,|2位置,4位置|(3), 1位置(1)\}
{5位置(5),∣2位置,4位置∣(3),1位置(1)}
遍历到
a
r
r
[
6
]
=
3
arr[6]=3
arr[6]=3,弹出5
位置,在栈中位置5
下面是
∣
2
位置
,
4
位置
∣
|2位置,4位置|
∣2位置,4位置∣,选最晚加入的4
位置,当前遍历到6
位置,所以
a
n
s
[
5
]
=
{
4
,
6
}
ans[5]=\{4,6\}
ans[5]={4,6}。位置6
进栈。stack
从栈顶到栈底
{
∣
2
位置
,
4
位置
,
6
位置
∣
(
3
)
,
1
位置
(
1
)
}
\{|2位置,4位置,6位置|(3), 1位置(1)\}
{∣2位置,4位置,6位置∣(3),1位置(1)}
遍历到
a
r
r
[
7
]
=
2
arr[7]=2
arr[7]=2,从栈中弹出
∣
2
位置
,
4
位置
,
6
位置
∣
|2位置,4位置,6位置|
∣2位置,4位置,6位置∣,在栈中这些位置下面是1
位置,当前是7
位置,
a
n
s
[
2
]
=
{
1
,
7
}
,
a
n
s
[
4
]
=
{
1
,
7
}
,
a
n
s
[
6
]
=
{
1
,
7
}
ans[2]=\{1,7\},ans[4]=\{1,7\},ans[6]=\{1,7\}
ans[2]={1,7},ans[4]={1,7},ans[6]={1,7}。位置7
进栈,stack
从栈顶到栈底
{
7
位置
(
2
)
,
1
位置
(
1
)
}
\{7位置(2), 1位置(1)\}
{7位置(2),1位置(1)}
遍历到
a
r
r
[
8
]
=
2
arr[8]=2
arr[8]=2,发现位置8
可以直接进栈,并且又是相等情况,stack
从栈顶到栈底
{
∣
7
位置
,
8
位置
∣
(
2
)
,
1
位置
(
1
)
}
\{|7位置,8位置|(2), 1位置(1)\}
{∣7位置,8位置∣(2),1位置(1)}
遍历完成后进入清算阶段:
弹出 ∣ 7 位置 , 8 位置 ∣ |7位置,8位置| ∣7位置,8位置∣, a n s [ 7 ] = { 1 , − 1 } , a n s [ 8 ] = { 1 , − 1 } ans[7]=\{1,-1\},ans[8]=\{1,-1\} ans[7]={1,−1},ans[8]={1,−1}
弹出1
位置,
a
n
s
[
1
]
=
{
−
1
,
−
1
}
ans[1]=\{-1,-1\}
ans[1]={−1,−1}
算法实现
/**
* 数组有重复情况
* @param arr
* @return
*/
public 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++) {
// 出现破坏从栈顶到栈底值递减单调性的情况
while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
// 栈顶的列表元素
List<Integer> popIs = stack.pop();
// 取位于下面位置的列表中,最晚加入的那个
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(
stack.peek().size() - 1);
// 列表重复元素集体清算
for (Integer popi : popIs) {
res[popi][0] = leftLessIndex;
res[popi][1] = i;
}
}
// 处理没有破坏单调性的情况
if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
// 当前元素与栈顶元素重复情况
stack.peek().add(Integer.valueOf(i));
} else {
// 构造列表,进队
ArrayList<Integer> list = new ArrayList<>();
list.add(i);
stack.push(list);
}
}
// 元素遍历结束,处理栈中剩余元素
while (!stack.isEmpty()) {
List<Integer> popIs = stack.pop();
// 取位于下面位置的列表中,最晚加入的那个
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(
stack.peek().size() - 1);
for (Integer popi : popIs) {
res[popi][0] = leftLessIndex;
res[popi][1] = -1;
}
}
return res;
}
问题扩展
定义:数组中累积和与最小值的乘积,假设叫做指标A。给定一个数组,请返回子数组中,指标A最大的值。
/**
* 单调栈处理
* 该位置左边离他最近且值比他小的,是不能扩的位置
* 该位置右边离他最近且值比他小的,是不能扩的位置
* @param arr
* @return
*/
public int max(int[] arr) {
int size = arr.length;
int[] sums = new int[size];
sums[0] = arr[0];
// 求前缀和
for (int i = 1; i < size; i++) {
sums[i] = sums[i - 1] + arr[i];
}
int max = Integer.MIN_VALUE;
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < size; i++) {
// 破坏单调性的情况,找到不能扩的位置边界
while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
// 以j位置作为最小值
int j = stack.pop();
// sums[stack.peek()] j左边不能扩的位置的前缀和
// sums[i - 1] - sums[stack.peek()] j位置为最小值开始右扩 --> 累加和
max = Math.max(max, (stack.isEmpty() ? sums[i - 1] : (sums[i - 1] - sums[stack.peek()])) * arr[j]);
}
// 遵循单调性,压栈
stack.push(i);
}
// 遍历完成处理剩余元素
while (!stack.isEmpty()) {
// 以j位置作为最小值
int j = stack.pop();
max = Math.max(max, (stack.isEmpty() ? sums[size - 1] : (sums[size - 1] - sums[stack.peek()])) * arr[j]);
}
return max;
}