这是我参与更文挑战的第 9 天,活动详情查看: 更文挑战
单调栈概念和特点
单调栈的定义:单调栈就是指栈中的元素必须是按照升序排列的栈,或者是降序排列的栈。
我们把升序排列的栈称为递增栈。降序排列的栈称为递减栈。
单调栈任何时候都要保持栈的有序性。
因此我们可以总结出单调栈的特点:
递增栈:
- 小数消除大数
- 栈中元素递增
递减栈:
- 大数消除小数
- 栈中元素递减
经典例子
【题目】一个整数数组 A,找到每个元素:右边第一个比我小的下标位置,没有则用 -1 表示。
输入:[5, 2]
输出:[1, -1]
解释:因为元素 5 的右边离我最近且比我小的位置应该是 A[1],最后一个元素 2 右边没有比 2 小的元素,所以应该输出 -1。
接口:int[] findRightSmall(int[] A);
【分析】
模拟
我们首先拿一个例子来模拟。如用 [1,2,4,9,4,0,5],可以得出 [5,5,5,4,5,-1,-1]
规律
这里我们是照着题意去寻找一个右边比它小的数的下标。可以发现,A[4] = 4 及 A[5] = 0,这两个数字多次被用到。并且:
A[4] 发现有左边 A[3],A[3] 就匹配成功;
结合 A[5] = 0 的例子,我们发现它会把比它大的数都进行匹配成功,但是 A[3] 除外;
A[3] 可以认为是匹配成功之后,被 A[4]消除了。
这时可以总结出:一个数总是想与左边比它大的数进行匹配,匹配到了之后,小的数会消除掉大的数。
匹配
当你发现要解决的题目有两个特点:
- 小的数要与大的数配对
- 小的数会消除大的数
你的脑海里应该联想到关于单调栈的特性。下面我们看看如何利用单调栈解决这道题目。
Step 1. 首先将 A[0] = 1 的下标 0 入栈。
Step 2. 将 A[1] = 2 的下标 1 入栈。满足单调栈。
Step 3. 将 A[2] = 4 的下标 2 入栈。满足单调栈。
Step 4. 将 A[3] = 9 的下标 3 入栈。满足单调栈。
Step 5. 将 A[4] = 4 的下标 4 入栈时,不满足单调性,需要将 A[3] = 9 从栈中弹出去。下标 4 将栈中下标 3 弹出栈,记录 A[3] 右边更小的是 index = 4。
Step 6. 将 A[5] = 0 的下标 5 入栈时,不满足单调性,需要将 A[4] = 4 从栈中弹出去。下标 5 将下标 4 弹出栈,记录 A[4] 右边更小的是 index = 5。A[5] = 0 会将栈中的下标 0, 1, 2 都弹出栈,因此也需要记录相应下标右边比其小的下标为 5,再将 A[5] = 0 的下标 5 放入栈中。
Step 7. 将 A[6] = 5 的下标 6 放入栈中。满足单调性。
Step 8. 此时,再也没有元素要入栈了,那么栈中的元素右边没有比其更小的元素。因此设置为 -1.
代码
public static int[] findRightSmall(int[] A) {
// 结果数组
int[] ans = new int[A.length];
// 注意,栈中的元素记录的是下标
Stack<Integer> t = new Stack();
for (int i = 0; i < A.length; i++) {
final int x = A[i];
// 每个元素都向左遍历栈中的元素完成消除动作
while (!t.empty() && A[t.peek()] > x) {
// 消除的时候,记录一下被谁消除了
ans[t.peek()] = i;
// 消除时候,值更大的需要从栈中消失
t.pop();
}
// 剩下的入栈
t.push(i);
}
// 栈中剩下的元素,由于没有人能消除他们,因此,只能将结果设置为-1。
while (!t.empty()) {
ans[t.peek()] = -1;
t.pop();
}
return ans;
}
时间复杂度:O(n)
空间复杂度:O(n)
小结
到这里我们可以得到一个有趣且非常有用的结论:数组中右边第一个比我小的元素的位置,求解用递增栈。
如果我们进一步归纳,会发现消除的时候,这里仍然是消除一个元素,保留一个元素。弹栈的时候,仍然是一直弹栈,直到满足某个条件为止。只是条件变成了直到元素大于栈顶元素。