算法之单调栈的使用总结(含接雨水、柱状图等经典问题)

本文介绍了如何使用单调栈解决一系列数组相关的编程问题,包括接雨水、柱状图最大矩形、最大宽度坡、表现良好的最长时间段、滑动窗口最大值、132模式和移掉k位数字等。通过具体的算法分析和代码实现,展示了单调栈在优化时间复杂度和空间复杂度上的应用。
摘要由CSDN通过智能技术生成

简述

单调栈是栈内元素满足单调性的栈。假设需要维护一个单调递增栈,新元素进栈时,为了维护栈的单调性,需要弹出大于等于新元素的栈顶元素。

  • 对于数组中的元素,可以找到左侧/右侧第一个比它大/小的元素。
  • 寻找某一子数组,使得子数组中的最小值乘以子数组的长度最大。
  • 寻找某一子数组,使得子数组中的最小值乘以子数组所有元素和最大。

通过以下题目,来学习单调栈解题的思想。

例题目录

题目来源:leetcode

例题

1、接雨水
题目描述:

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

示例:

输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6

算法

维护一个单调递减栈,当前元素大于栈顶元素时,弹出栈顶元素,计算当前元素与栈顶的前一个元素能接雨水的大小。

动画如下:

接雨水.gif

代码
class Solution:
    def trap(self, height: List[int]) -> int:
        stack = []
        ans = 0
        for cur in range(len(height)):
            while stack and height[cur] > height[stack[-1]]:
                top = stack.pop()
                if not stack:
                    break
                distance  = cur - stack[-1] - 1
                h = min(height[cur], height[stack[-1]]) - height[top]
                ans += distance * h
            stack.append(cur)
            cur += 1
        return ans

时间复杂度:O(n)

这道题还可以用双指针解答,代码如下:

class Solution {
    public int trap(int[] height) {
        int n = height.length, res = 0;
        int left_m = 0, right_m = 0, left = 0, right = n - 1;
        while (left < right) {
            left_m = Math.max(left_m, height[left]);
            right_m = Math.max(right_m, height[right]);
            if (left_m < right_m) {
                res += Math.min(left_m, right_m) - height[left];
                left++;
            } else {
                res += Math.min(left_m, right_m) - height[right];
                right--;
            }
        }
        return res;
    }
}
2、柱状图中最大的矩形
题目描述:

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。

算法

维护一个单调递增栈,当前元素小于等于栈顶元素时,弹出栈顶元素,并计算栈顶元素与栈顶的前一个元素之间矩形面积的大小。

高度由弹出的元素决定,宽度由两边的最矮高度决定。

动画:

代码
class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        stack = [-1]
        n = len(heights)
        ans = 0
        for i in range(n):
            while stack[-1] != -1 and heights[i] <= heights[stack[-1]]:
                index = stack.pop()
                ans = max(ans, (i - stack[-1] - 1) * heights[index])

            stack.append(i)

        while stack[-1] != -1:
            index = stack.pop()
            ans = max(ans, (n - stack[-1] - 1) * heights[index])

        return ans

每个元素最多入栈出栈两次,因此时间复杂度为O(n).

3、最大宽度坡
题目描述:

给定一个整数数组 A,坡是元组 (i, j),其中 i < j 且 A[i] <= A[j]。这样的坡的宽度为 j - i。

找出 A 中的坡的最大宽度,如果不存在,返回 0 。

算法

1 维护一个单调递减栈,其中第一个元素是A中第一个元素,最后一个元素是A的最小值。由于需要计算长度,所以栈中存储A的索引。

2 从后向前遍历A,当元素大于栈顶元素时,计算一次最大宽度坡,并弹出(因为再往前面遍历宽度肯定会减少),由于当栈顶索引等于当前遍历到的元素的索引时,肯定会被弹出,所以没有必要判断栈顶索引是否小于等于当前遍历到的索引。

代码
class Solution {
    public int maxWidthRamp(int[] A) {
        Stack<Integer> stack = new Stack<>();
        int n = A.length;
        // 得到A的单调递减栈
        for (int i = 0; i < n; i++) {
            if (stack.isEmpty() || A[stack.peek()] > A[i]) {
                stack.add(i);
            }
        }
        int res = 0, i = n - 1;
        while (i > res) {
            while (!stack.isEmpty() && A[stack.peek()] <= A[i]) {
                res = Math.max(res, i - stack.pop());
            }
            i--;
        }
        return res;
    }
}
4、表现良好的最长时间段
题目描述:

给你一份工作时间表 hours,上面记录着某一位员工每天的工作小时数。

我们认为当员工一天中的工作小时数大于 8 小时的时候,那么这一天就是「劳累的一天」。

所谓「表现良好的时间段」,意味在这段时间内,「劳累的天数」是严格 大于「不劳累的天数」。

请你返回「表现良好时间段」的最大长度。

示例 1:
输入:hours = [9,9,6,0,6,6,9]
输出:3
解释:最长的表现良好时间段是 [9,9,6]。

代码
class Solution {
    public int longestWPI(int[] hours) {
        int n = hours.length;
        // 大于8编码为1,小于8编码为-1
        int[] hourCode = new int[n];
        for (int i = 0; i < n; i++) {
            hourCode[i] = hours[i] > 8 ? 1 : -1;
        }
        // 求编码的前缀和
        int[] preSum = new int[n+1];
        int total = 0;
        for (int i = 0; i < n; i++) {
            preSum[i] =  total;
            total += hourCode[i];
        }
        preSum[n] = total;
        // 单调递减栈
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < n + 1; i++) {
            if (stack.isEmpty() || preSum[stack.peek()] > preSum[i]) {
                stack.add(i);
            }
        }
        // 求最长坡度,倒序遍历
        int res = 0;
        for (int i = n; i >= 0; i--) {
            while (!stack.isEmpty() && preSum[stack.peek()] < preSum[i]) {
                res = Math.max(res, i - stack.pop());
            }
        }
        return res;
    }
}
5、滑动窗口的最大值
题目描述:

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]

算法
  1. 暴力解法。对于每一个元素nums[i],寻找nums[i:i+k]范围内的最大值。时间复杂度为O(kn)
  2. 优先队列(大顶堆)。
    首先往大顶堆中放入k个元素,此时的堆顶元素是所求的第一个滑动窗口的值。
    后面继续往大顶堆放入元素,同时取此时的最大值。取最大值的时候检查堆顶是否是在此时的滑动窗口之内,如果不是,则丢弃继续寻找。因此堆中存放的数据应该包含索引信息。
    时间复杂度O(nlogn),空间复杂度O(n)
  3. 单调栈
    维护一个单调递减双向队列。遍历nums,如果当前num大于队列尾的元素,则弹出所有大于num的数;同时如果当前元素等于队列首的元素,则poll队列首的元素。
代码
public int[] maxSlidingWindow(int[] nums, int k) {
    int n = nums.length;
    if (n == 0 || k == 0) return new int[0];
    Deque<Integer> deque = new LinkedList<>();
    int[] res = new int[n - k + 1];
    for (int i = 0; i < k; i++) {
        while (!deque.isEmpty() && nums[i] > deque.peekLast()) {
            deque.pollLast();
        }
        deque.addLast(nums[i]);
    }
    res[0] = deque.peekFirst();
    for (int i = k; i < n; i++) {
        if (!deque.isEmpty() && nums[i - k] == deque.peekFirst()) {
            deque.pollFirst();
        }
        while (!deque.isEmpty() && nums[i] > deque.peekLast()) {
            deque.pollLast();
        }
        deque.addLast(nums[i]);
        res[i - k + 1] = deque.peekFirst();
    }
    return res;
}

每个元素最多入队出队一次,因此时间复杂度O(n),双向队列的长度最大为k,空间复杂度为O(k).

6、132模式
题目描述:

给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]、nums[j] 和 nums[k] 组成,并同时满足:i < j < k 和 nums[i] < nums[k] < nums[j] 。

如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false 。

进阶:很容易想到时间复杂度为 O(n^2) 的解决方案,你可以设计一个时间复杂度为 O(n logn) 或 O(n) 的解决方案吗?

算法

很容易想到两层循环来解决,但是似乎可以用单调栈来进一步减少时间复杂度。

做法:

  1. 维护一个 n u m s [ : i ] nums[:i] nums[:i] 的最小数组 m i n L i s t minList minList m i n L i s t [ i ] minList[i] minList[i] 表示 n u m s [ : i ] nums[:i] nums[:i] 中的最小值;
  2. 从后往前遍历 nums 的同时,维护一个单调递减栈 stack;
  3. 如果当前元素大于栈顶元素,则弹出栈顶元素 topNum,同时比较 topNum 和 minList[i] 的大小,如果 topNum > minList[i],说明找到了“132模式”。
代码
public class Solution456 {
    // 132模式
    public boolean find132pattern(int[] nums) {
        int n = nums.length;
        int[] minList = new int[n];
        minList[0] = nums[0];
        for (int i = 1; i < n; i++) {
            minList[i] = Math.min(minList[i - 1], nums[i]);
        }
        Stack<Integer> stack = new Stack<>();
        for (int i = n - 1; i >= 0; i--) {
        	int topNum = Integer.MIN_VALUE;
			while (!stack.isEmpty() && nums[i] > stack.peek()) {
				topNum = stack.pop();
			}
			if (topNum > minList[i]) {
				return true;
			}
			stack.add(nums[i]);
		}
        return false;
    }
}

建立 minList 时间复杂度为 O(n),寻找132模式过程中每个元素最多进栈出栈一次,时间复杂度 O(n),所以最终时间复杂度为 O(n).

7、移掉k位数字
题目描述:

给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小。

注意:

num 的长度小于 10002 且 ≥ k。
num 不会包含任何前导零。

示例 1 :

输入: num = “1432219”, k = 3
输出: “1219”
解释: 移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219。

算法

维护一个单调递增双向队列,同时记录从队列中弹出的元素个数,当弹出元素个数为k时,则得到了最终的结果。

由于最终的输出是字符串,所以还需要从队列头依次取出元素,所以不用栈,而是用双向队列。

同时还需注意删除前导0.

代码
class Solution {
    public String removeKdigits(String num, int k) {
        Deque<Character> deque = new LinkedList<>();
        for (int i = 0; i < num.length(); i++) {
            while (k > 0 && !deque.isEmpty() && num.charAt(i) < deque.peekLast()) {
                deque.pollLast();
                k--;
            }
            deque.addLast(num.charAt(i));
        }
        while (!deque.isEmpty() && k > 0) {
            deque.pollLast();
            k--;
        }
        while (!deque.isEmpty() && deque.peekFirst() == '0') {
            deque.pollFirst();
        }
        if (deque.isEmpty()) {
            return "0";
        }
        StringBuilder builder = new StringBuilder();
        while (!deque.isEmpty()) {
            builder.append(deque.pollFirst());
        }
        return builder.toString();
    }
}

每个元素最多进队出队一次,所以时间复杂度为O(n).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值