一篇文章入门单调栈
1. 基本介绍
1.1 问题引出
一种特别设计的栈结构,为了解决如下的问题:
给定一个可能含有重复值的数组arr,i位置的数一定存在如下两个信息
arr[i]的左侧离 i 最近并且小于(或者大于)arr[i]的数在哪?arr[i]的右侧离 i 最近并且小于(或者大于)arr[i]的数在哪?
如果想得到 arr 中所有位置的两个信息,怎么能让得到信息的过程尽量快呢?
1.2 结构解析
我们先考虑没有重复值的情况。
该结构以 栈 为基础,只不过在更新元素的时候要求保持栈中元素的单调。栈中存储这元素的下标(位置)。
以引出的问题为例,假设我们有数组 arr:[3, 4, 1, 6, 7, 9, 0] ,那么我们从下标 0 开始,假设我们以“0->3”表示栈中存储的是下标 0 对应的数值元素 3:
- 起初栈中没有元素,直接将 3 入栈,此时栈中元素为
[0->3]; - i=1,由于 4 比 3 大,栈满足单调结构,所以 4 入栈。此时栈中为
[0->3,1->4]; - i=2, 1 小于 4,所以弹出 4,此时栈中为
[0->3]:- 对 4 的信息进行记录,4 左边最近的最小值就为此时栈的顶部 3,右边最小最近元素就为那个还未入栈的 1;
- 1 仍比栈顶元素 3 大,所以继续弹出 3,记录 3 的信息,然后将 1 入栈;
- 此时栈中元素为
[2->1];
- i=3, 6 由于比 1 大直接入栈,此时栈中元素为
[2->1,3->6]; - 之后的元素同理;
建议自己对照流程画一画!更加清晰。
1.3 构建流程
由以上的例子,我们可以很容易的抽象出流程:
- 栈空直接入栈;
- 栈不空则与栈顶比较,弹出元素,并且记录弹出元素信息,直到加入满足单调条件或栈空,停止弹出;
- 弹出元素时,新栈顶就是其左侧最近的值,待入栈元素就是其右侧最近的值;
- 若以上流程执行完毕后,栈中还有元素,则弹出所有元素,并且记录相关信息(都没有右边的最近的最小值);
2. 注意事项
- 为什么这个流程可行?为证明这个问题,先设假如栈顶元素是 b,b 的下一个元素是 a,现在 c 出现准备破坏了栈的单调性,准备弹出 b。我们要聚焦两个问题:一是凭什么 c 就是 b 右边的最近的小于值?二是,凭什么 a 就是 b 左边最近小于值?
- 先证明 c 是 b 右边最近小于值。首先 c 在数组中的位置一定实在 b 后面的。假如 b 到 c 之间有小于 b 的值,那么一定会造成 b 弹出,所以 b 到 c 之间一定没有小于 b 的值,也即是 c 是 b 右边最近小于值。证毕;
- 再证明 a 是 b 左边的最小值。先证明 a 到 b 之间一定没有小于 b 大于 a 的值,假如真的有小于 b 的值,那么栈中 a 和 b 一定不相邻;再证明 a 到 b 之间一定没有小于 a 的值,假如真的小于 a 的值,那么栈的结构将被破坏,也就是 a 必然会被弹出,ab 也就不相邻了,证毕。
- 怎么解决重复值的问题?利用链表,若有重复值元素准备压入,就把栈中的相同元素与重复元素的下标合并成一个链表。若找到了小元素就一起弹出一起结算。因为,若在发现重复元素压入的时候直接弹出那个栈中的重复元素会使得那个弹出的元素还没有找到最小值就结算了,这显然是错误的。
- 栈中尽量只记录元素的下标(位置);
- 栈中的元素弹出后结算答案;
3. 结构实现
我们这里利用一个 n*2 的二维表来存储结果 \(a_{i,0}\) 就代表下标为 i 左边最近的小于元素,\(a_{i,1}\) 就代表 i 右边最近的小于元素。
3.1 不解决重复值问题
/**
* 只适用于无重复值的时候
*
* @param arr
* @return
*/private static 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[i] < arr[stack.peek()]) {
Integer index = stack.pop();
res[index][0] = stack.isEmpty() ? -1 : stack.peek();
res[index][1] = i;
}
stack.push(i);
}
while (!stack.isEmpty()) {
Integer index = stack.pop();
res[index][0] = stack.isEmpty() ? -1 : stack.peek();
res[index][1] = -1;
}
return res;
}
3.2 解决重复值问题
/**
* 支持重复值
*
* @param arr
* @return
*/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++) {
//弹出不符合单调条件的链表
while (!stack.isEmpty() && arr[i] < arr[stack.peek().get(0)]) {
List<Integer> list = stack.pop();
for (Integer integer : list) {
//stack.peek().get(stack.peek().size() - 1) 代表栈顶链表的最后一个元素
res[integer][0] = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
res[integer][1] = i;
}
}
//如果相同就加入同一个队列
if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
stack.peek().add(i);
} else { //若栈为空或者不同就直接入栈
ArrayList<Integer> list = new ArrayList<>();
list.add(i);
stack.push(list);
}
}
//弹出剩余元素
while (!stack.isEmpty()) {
List<Integer> curList = stack.pop();
for (Integer integer : curList) {
res[integer][0] = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
res[integer][1] = -1;
}
}
return res;
}
解读:
- 为什么在得出结果的时候要取栈顶链表的最后一个元素,即
res[integer][0] = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);?因为我们填结果表 res 不是根据数组值来填的,而是根据下标,下标就代表着位置,我们记录的是每一个位置的数的情况。所以为了保证左边最小元素位置的正确性,我们必须要这么做。例如,有数组arr[0,0,1,1],那么一定在某一时刻,栈元素为[[0->0,1->0],[2->1,3->1]],我们自然希望2->1记录的结果是1->0,而不是0->0; - 为什么将重复值加入链表,并且从前往后弹出的操作不会破坏栈的单调结构?首先,栈中仍然是满足单调性,假设有一个长度大于 2 的链表被弹出,那么其下方的链表所代表的元素一定比其小,而使它被弹出的元素一定比该链表所代表的元素大;其次,这个单链表能构成,说明这几个元素(该链表的各元素)之间一定没有大于该链表代表元素小,所以仍然满足结构;

浙公网安备 33010602011771号