基本介绍
栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈。单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。
现在给你出这么一道题:输入一个数组nums
,请你返回一个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。函数签名如下:
int[] nextGreaterElement(int[] nums);
比如说,输入一个数组nums = [2,1,2,4,3]
,你返回数组[4,2,4,-1,-1]
。因为第一个 2 后面比 2 大的数是 4; 1 后面比 1 大的数是 2;第二个 2 后面比 2 大的数是 4; 4 后面没有比 4 大的数,填 -1;3 后面没有比 3 大的数,填 -1。
这道题的暴力解法很好想到,就是对每个元素后面都进行扫描,找到第一个更大的元素就行了。但是暴力解法的时间复杂度是O(n^2)
。
这个问题可以这样抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「2」的下一个更大元素呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的下一个更大元素,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。
这个情景很好理解吧?带着这个抽象的情景,先来看下代码。
public int[] nextGreaterElement(int[] nums) {
int n = nums.length;
// 存放答案的数组
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
// 倒着往栈里放
for (int i = n - 1; i >= 0; i--) {
// 判定个子高矮
while (!s.isEmpty() && s.peek() <= nums[i]) {
// 矮个起开,反正也被挡着了。。。
s.pop();
}
// nums[i] 身后的更大元素
res[i] = s.isEmpty() ? -1 : s.peek();
s.push(nums[i]);
}
return res;
}
这就是单调队列解决问题的模板。for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。while 循环是把两个「个子高」元素之间的元素排除,因为他们的存在没有意义,前面挡着个「更高」的元素,所以他们不可能被作为后续进来的元素的下一个更大元素了。
这个算法的时间复杂度不是那么直观,如果你看到 for 循环嵌套 while 循环,可能认为这个算法的复杂度也是O(n^2)
,但是实际上这个算法的复杂度只有O(n)
。
分析它的时间复杂度,要从整体来看:总共有n
个元素,每个元素都被push
入栈了一次,而最多会被pop
一次,没有任何冗余操作。所以总的计算规模是和元素规模n
成正比的,也就是O(n)
的复杂度。
题目
496. 下一个更大元素 I
这道题给你输入两个数组nums1
和nums2
,让你求nums1
中的元素在nums2
中的下一个更大元素,函数签名如下:
int[] nextGreaterElement(int[] nums1, int[] nums2)
其实和把我们刚才的代码改一改就可以解决这道题了,因为题目说nums1
是nums2
的子集,那么我们先把nums2
中每个元素的下一个更大元素算出来存到一个映射里,然后再让nums1
中的元素去查表即可:
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
// 记录 nums2 中每个元素的下一个更大元素
int[] greater = nextGreaterElement(nums2);
// 转化成映射:元素 x -> x 的下一个最大元素
HashMap<Integer, Integer> greaterMap = new HashMap<>();
for (int i = 0; i < nums2.length; i++) {
greaterMap.put(nums2[i], greater[i]);
}
// nums1 是 nums2 的子集,所以根据 greaterMap 可以得到结果
int[] res = new int[nums1.length];
for (int i = 0; i < nums1.length; i++) {
res[i] = greaterMap.get(nums1[i]);
}
return res;
}
public int[] nextGreaterElement(int[] nums) {
int n = nums.length;
// 存放答案的数组
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
// 倒着往栈里放
for (int i = n - 1; i >= 0; i--) {
// 判定个子高矮
while (!s.isEmpty() && s.peek() <= nums[i]) {
// 矮个起开,反正也被挡着了。。。
s.pop();
}
// nums[i] 身后的更大元素
res[i] = s.isEmpty() ? -1 : s.peek();
s.push(nums[i]);
}
return res;
}
739. 每日温度
给你一个数组temperatures
,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0。函数签名如下:
int[] dailyTemperatures(int[] temperatures);
比如说给你输入temperatures = [73,74,75,71,69,76]
,你返回[1,1,3,2,1,0]
。因为第一天 73 华氏度,第二天 74 华氏度,比 73 大,所以对于第一天,只要等一天就能等到一个更暖和的气温,后面的同理。
这个问题本质上也是找下一个更大元素,只不过现在不是问你下一个更大元素的值是多少,而是问你当前元素距离下一个更大元素的索引距离而已。
相同的思路,直接调用单调栈的算法模板,稍作改动就可以,直接上代码吧:
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
// 这里放元素索引,而不是元素
Stack<Integer> s = new Stack<>();
/* 单调栈模板 */
for (int i = n - 1; i >= 0; i--) {
while (!s.isEmpty() && temperatures[s.peek()] <= temperatures[i]) {
s.pop();
}
// 得到索引间距
res[i] = s.isEmpty() ? 0 : (s.peek() - i);
// 将索引入栈,而不是元素
s.push(i);
}
return res;
}
}
503. 下一个更大元素 II
同样是求下一个更大元素,现在假设给你的数组是个环形的,如何处理?力扣第 503 题「下一个更大元素 II」就是这个问题:输入一个「环形数组」,请你计算其中每个元素的下一个更大元素。
比如输入[2,1,2,4,3]
,你应该返回[4,2,4,-1,4]
,因为拥有了环形属性,最后一个元素 3 绕了一圈后找到了比自己大的元素 4。
我们一般是通过 % 运算符求模(余数),来模拟环形特效:
int[] arr = {1,2,3,4,5};
int n = arr.length, index = 0;
while (true) {
// 在环形数组中转圈
print(arr[index % n]);
index++;
}
这个问题肯定还是要用单调栈的解题模板,但难点在于,比如输入是[2,1,2,4,3]
,对于最后一个元素 3,如何找到元素 4 作为下一个更大元素。
对于这种需求,常用套路就是将数组长度翻倍:
这样,元素 3 就可以找到元素 4 作为下一个更大元素了,而且其他的元素都可以被正确地计算。
有了思路,最简单的实现方式当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,我们可以不用构造新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果。
代码如下:
class Solution {
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
// 数组长度加倍模拟环形数组
for (int i = 2 * n - 1; i >= 0; i--) {
// 索引 i 要求模,其他的和模板一样
while (!s.isEmpty() && s.peek() <= nums[i % n]) {
s.pop();
}
res[i % n] = s.isEmpty() ? -1 : s.peek();
s.push(nums[i % n]);
}
return res;
}
}
84. 柱状图中最大的矩形
以每个柱形图作为最终的高,分别求出对应的面积,进行比较,求出最终的最大矩形,但是注意,计算以每个柱形最高的面积时,需要找到左边比它高度低的最近边界以及右边比它低的最近边界,这其实就需要用到单调栈!!!
代码如下:
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int largestArea = 0;
int[] leftArr = new int[n];
int[] rightArr = new int[n];
Stack<Integer> stack = new Stack<>();
//计算左边界的索引
for(int i = 0; i < n; i++) {
while(!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
stack.pop();
}
leftArr[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
stack.clear();
//计算有边界的索引
for(int j = n-1; j >= 0; j--) {
while(!stack.isEmpty() && heights[stack.peek()] >= heights[j]) {
stack.pop();
}
rightArr[j] = stack.isEmpty() ? n : stack.peek();
stack.push(j);
}
//比较以每个柱形图作为高的面积
for(int k = 0; k < n; k++) {
int tempArea = (rightArr[k] - leftArr[k] - 1) * heights[k];
largestArea = Math.max(largestArea, tempArea);
}
return largestArea;
}
}
85. 最大矩形
解法一:暴力法
遍历每个点,求以这个点为矩阵右下角的所有矩阵面积。如下图的两个例子,橙色是当前遍历的点,然后虚线框圈出的矩阵是其中一个矩阵。
怎么找出这样的矩阵呢?如下图,如果我们知道了以这个点结尾的连续 1 的个数的话,问题就变得简单了。
-
首先求出高度是 1 的矩形面积,也就是它自身的数,如图中橙色的 4,面积就是 4。
-
然后向上扩展一行,高度增加一,选出当前列最小的数字,作为矩阵的宽,求出面积,对应上图的矩形框。
-
然后继续向上扩展,重复步骤 2。
按照上边的方法,遍历所有的点,求出所有的矩阵就可以了。
以橙色的点为右下角,高度为 1。
class Solution {
public int maximalRectangle(char[][] matrix) {
int m = matrix.length;
if (m == 0) {
return 0;
}
int n = matrix[0].length;
//存储当前行,自己往左边数有多少个连续1
int[][] left = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '1') {
left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
}
}
}
int ret = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '0') {
continue;
}
int width = left[i][j];
int area = width;
//当前列,往上行遍历
for (int k = i - 1; k >= 0; k--) {
width = Math.min(width, left[k][j]);
area = Math.max(area, (i - k + 1) * width);
}
ret = Math.max(ret, area);
}
}
return ret;
}
}
解法二:单调栈
其实你会注意到,当选择matrix[i][j]
时候,选择left[i][j]作为矩形的宽度,利用单调栈往上和往下寻找第一个小于当前left[i][j]的行号,然后确定矩形的高度,通过比较,最终得到结果。
代码如下:
class Solution {
public int maximalRectangle(char[][] matrix) {
int m = matrix.length;
if (m == 0) {
return 0;
}
int n = matrix[0].length;
int[][] left = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '1') {
left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
}
}
}
int ret = 0;
for (int j = 0; j < n; j++) {
int[] up = new int[m];
int[] down = new int[m];
//找到上边界,即找到第一个比left[i][j]小的行
Stack<Integer> stack = new Stack<Integer>();
for (int i = 0; i < m; i++) {
while (!stack.isEmpty() && left[stack.peek()][j] >= left[i][j]) {
stack.pop();
}
up[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
stack.clear();
//找到下边界 即找到第一个比left[i][j]小的行
for (int i = m - 1; i >= 0; i--) {
while (!stack.isEmpty() && left[stack.peek()][j] >= left[i][j]) {
stack.pop();
}
down[i] = stack.isEmpty() ? m : stack.peek();
stack.push(i);
}
//以left[i][j]作为矩形的宽度,上下边界确定矩形高度
for (int i = 0; i < m; i++) {
int height = down[i] - up[i] - 1;
int area = height * left[i][j];
ret = Math.max(ret, area);
}
}
return ret;
}
}