何为单调栈
栈内元素非递增或者非递减。另一种说法是从栈底到栈顶递增或者递减。在很多情况下,可能会出现相同的数字元素,所以称之为非递增或者非递减栈比递增、递减栈更合适。
显而易见,从单调栈的这种结构很容易联想到,在算法中,合理运用单调栈,能够将O(n^2)的时间复杂度优化到O(n),这就是技巧。相对的,空间复杂度会增加,因为需要动态维护一个栈。这里需要明白一点,算法里面,都是时间和空间的取舍,所谓的时空间转换指的就是这个,所以要根据具体场景去选择。
适用范围
求一个数组元素的下一个最大或最小值、判断当前元素符合某种条件的左右边界…
第一个例子,求第一个数组中的x元素在第二个数组中的位置右边的第一大元素。
对应leetcode:下一个更大元素 I
数组1:[4,2,1],数组2:[1,3,4,2]。很明显,要求的是右边的第一大元素,说明需要先知道当前元素右边的数字,所以从右边开始遍历比较方便;反之,如果要求当前元素左边的第一大元素,那么就从左往右遍历(重要)。
解法1的思想是,从右往左遍历,循环出栈后,就认为栈顶元素就是下一个最大的值。过程:
- 栈空,2入栈
- 4 > 2,2出栈,栈为空,没有比4大的元素,4入栈
- 3 < 4,且栈不空,栈顶元素4即为3的右边第一大元素,3入栈
- 1 < 3,且栈不空,栈顶元素3即为1的右边第一大元素,1入栈
代码:
void demo() {
int[] nums1 = {4, 1, 2};
int[] nums2 = {1, 3, 4, 2};
int[] res = new int[nums1.length];
HashMap<Integer, Integer> map = new HashMap<>();
Deque<Integer> stack = new LinkedList<>();
for (int i = nums2.length - 1; i >= 0; --i) {
while (!stack.isEmpty() && nums2[i] >= stack.peek()) {
stack.pop();
}
map.put(nums2[i], stack.isEmpty() ? -1 : stack.peek());
stack.push(nums2[i]);
}
for (int i = 0; i < nums1.length; ++i) {
res[i] = map.get(nums1[i]);
}
for (int ele : res) {
System.out.print(ele + " ");
}
}
解法2的思想是,仍然从左往右遍历,当栈不空或者栈顶元素比当前元素小,循环出栈,出栈元素的下一个最大值即为当前遍历的值。过程:
- 栈空,1入栈
- 栈不空且3 > 1,1出栈,1的下一个最大元素为3,3入栈
- 栈不空且4 > 3,3出栈,3的下一个最大元素为4,4入栈
- 栈不空,但2 < 4,4的下一个最大元素为-1,2入栈
- 栈内元素全部没有右边的下一个最大元素,都为-1
代码:
void demo() {
int[] nums1 = {4, 1, 2};
int[] nums2 = {1, 3, 4, 2};
int[] res = new int[nums1.length];
HashMap<Integer, Integer> map = new HashMap<>();
Deque<Integer> stack = new LinkedList<>();
for (int i = 0; i < nums2.length; ++i) {
while (!stack.isEmpty() && nums2[i] > stack.peek()) {
map.put(stack.pop(), nums2[i]);
}
stack.push(nums2[i]);
}
for (int i = 0; i < nums1.length; ++i) {
res[i] = map.getOrDefault(nums1[i], -1);
}
for (int e : res) {
System.out.print(e + " ");
}
}
入门第二个例子,求nums 中每个元素的下一个更大元素,这意味着应该循环去遍历数组。如数组[1,2,1],直接从前往后遍历,使用上个例题中的第二个方法,但是因为不需要元素的对应关系,所以我们可以单调栈中存下标。过程由于是存下标,就不写了。
对应leetcode:下一个更大元素 II
代码如下:
void demo() {
int[] nums = {1, 2, 1};
int n = nums.length;
Deque<Integer> stack = new LinkedList<>();
int[] res = new int[n];
Arrays.fill(res, -1);
for (int i = 0; i < 2 * n - 1; ++i) {
while (!stack.isEmpty() && nums[i % n] > nums[stack.peek()]) {
res[stack.pop()] = nums[i % n];
}
stack.push(i % n);
}
for (int e : res) {
System.out.print(e + " ");
}
}
最后再来一个求每个元素左右边界的例子。
对应leetcode:84. 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。 例如[2,1,5,6,2,3],直接从左往右遍历,找到每个元素的左边界;再从右往左遍历,找到每个元素的右边界。那么每个元素的左右边界宽度与当前元素的乘积即为当前元素所能组成的最大区间,比较每个元素所能组成的区间面积即可得到整个数组的最大区间。从左往右过程:
- 栈空,2入栈,2的左边界是-1
- 栈不空,且1 < 2,2出栈,栈空,1的左边界是-1,1入栈
- 栈不空,且5 > 1,栈不空,5的左边界是1,5入栈
- 栈不空,且6 > 5,栈不空,6的左边界是2,6入栈
- 栈不空,且2 < 6,6出栈,5出栈,2的左边界是1,2入栈
- 栈不空,且3 > 2,栈不空,3的左边界是4,3入栈
如此,即可得到数组每一个元素的左边界,相同的方法,求出右边界。
代码:
public int largestRectangleArea(int[] heights) {
int n = heights.length;
Deque<Integer> stack = new LinkedList<>();
int[] leftArray = new int[n];
int[] rightArray = new int[n];
for (int i = 0; i < n; ++i) {
while (!stack.isEmpty() && heights[i] <= heights[stack.peek()]) {
stack.pop();
}
leftArray[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
stack.clear();
for (int i = n - 1; i >= 0; --i) {
while (!stack.isEmpty() && heights[i] <= heights[stack.peek()]) {
stack.pop();
}
rightArray[i] = stack.isEmpty() ? n : stack.peek();
stack.push(i);
}
int res = 0;
for (int i = 0; i < n; ++i) {
res = Math.max(res, (rightArray[i] - leftArray[i] - 1) * heights[i]);
}
return res;
}