单调栈题目:接雨水

题目

标题和出处

标题:接雨水

出处:42. 接雨水

难度

6 级

题目描述

要求

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

示例

示例 1:

示例 1

输入: height   =   [0,1,0,2,1,0,1,3,2,1,2,1] \texttt{height = [0,1,0,2,1,0,1,3,2,1,2,1]} height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6 \texttt{6} 6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] \texttt{[0,1,0,2,1,0,1,3,2,1,2,1]} [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 \texttt{6} 6 个单位的雨水(蓝色部分表示雨水)。

示例 2:

输入: height   =   [4,2,0,3,2,5] \texttt{height = [4,2,0,3,2,5]} height = [4,2,0,3,2,5]
输出: 9 \texttt{9} 9

数据范围

  • n = height.length \texttt{n} = \texttt{height.length} n=height.length
  • 1 ≤ n ≤ 2 × 10 4 \texttt{1} \le \texttt{n} \le \texttt{2} \times \texttt{10}^\texttt{4} 1n2×104
  • 0 ≤ height[i] ≤ 10 5 \texttt{0} \le \texttt{height[i]} \le \texttt{10}^\texttt{5} 0height[i]105

解法一

思路和算法

下标 i i i 处的雨水能达到的高度等于其两边的最大高度的最小值,下标 i i i 处的雨水能达到的高度与 height [ i ] \textit{height}[i] height[i] 之差即为下标 i i i 处的雨水量。只要得到每个下标处的两边最大高度,即可得到能接的雨水总量。

朴素的解法是对于每个下标分别向两边遍历找到最大高度,对于长度为 n n n 的数组 height \textit{height} height,每个下标需要 O ( n ) O(n) O(n) 的时间找到两边的最大高度,总时间复杂度是 O ( n 2 ) O(n^2) O(n2)。如果可以预计算每个下标两边的最大高度,则可以将总时间复杂度降到 O ( n ) O(n) O(n)

创建两个长度为 n n n 的数组 leftHeight \textit{leftHeight} leftHeight rightHeight \textit{rightHeight} rightHeight,对于 0 ≤ i < n 0 \le i < n 0i<n leftHeight [ i ] \textit{leftHeight}[i] leftHeight[i] 表示 height \textit{height} height 在下标范围 [ 0 , i ] [0, i] [0,i] 内的最大元素, rightHeight [ i ] \textit{rightHeight}[i] rightHeight[i] 表示 height \textit{height} height 在下标范围 [ i , n − 1 ] [i, n - 1] [i,n1] 内的最大元素。

数组 leftHeight \textit{leftHeight} leftHeight rightHeight \textit{rightHeight} rightHeight 的计算方法如下:

  • 对于 leftHeight \textit{leftHeight} leftHeight leftHeight [ 0 ] = height [ 0 ] \textit{leftHeight}[0] = \textit{height}[0] leftHeight[0]=height[0],当 1 ≤ i < n 1 \le i < n 1i<n leftHeight [ i ] = max ⁡ ( leftHeight [ i − 1 ] , height [ i ] ) \textit{leftHeight}[i] = \max(\textit{leftHeight}[i - 1], \textit{height}[i]) leftHeight[i]=max(leftHeight[i1],height[i]),从左到右依次计算 leftHeight \textit{leftHeight} leftHeight 的值;

  • 对于 rightHeight \textit{rightHeight} rightHeight rightHeight [ n − 1 ] = height [ n − 1 ] \textit{rightHeight}[n - 1] = \textit{height}[n - 1] rightHeight[n1]=height[n1],当 0 ≤ i < n − 1 0 \le i < n - 1 0i<n1 rightHeight [ i ] = max ⁡ ( rightHeight [ i + 1 ] , height [ i ] ) \textit{rightHeight}[i] = \max(\textit{rightHeight}[i + 1], \textit{height}[i]) rightHeight[i]=max(rightHeight[i+1],height[i]),从右到左依次计算 rightHeight \textit{rightHeight} rightHeight 的值。

得到数组 leftHeight \textit{leftHeight} leftHeight rightHeight \textit{rightHeight} rightHeight 之后,即可计算每个下标处的雨水量。下标 i i i 处的雨水能到达的高度是 min ⁡ ( leftHeight [ i ] , rightHeight [ i ] ) \min(\textit{leftHeight}[i], \textit{rightHeight}[i]) min(leftHeight[i],rightHeight[i]),因此下标 i i i 处的雨水量是 min ⁡ ( leftHeight [ i ] , rightHeight [ i ] ) − height [ i ] \min(\textit{leftHeight}[i], \textit{rightHeight}[i]) - \textit{height}[i] min(leftHeight[i],rightHeight[i])height[i]。遍历全部下标,计算每个下标处的雨水量,即可得到能接的雨水总量。

下图是示例 1 使用该解法的体现。红色和黄色分别表示 leftHeight \textit{leftHeight} leftHeight rightHeight \textit{rightHeight} rightHeight 的值,橙色表示能接的雨水。

图 1

代码

class Solution {
    public int trap(int[] height) {
        int n = height.length;
        int[] leftHeight = new int[n];
        leftHeight[0] = height[0];
        for (int i = 1; i < n; i++) {
            leftHeight[i] = Math.max(leftHeight[i - 1], height[i]);
        }
        int[] rightHeight = new int[n];
        rightHeight[n - 1] = height[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            rightHeight[i] = Math.max(rightHeight[i + 1], height[i]);
        }
        int amount = 0;
        for (int i = 0; i < n; i++) {
            amount += Math.min(leftHeight[i], rightHeight[i]) - height[i];
        }
        return amount;
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 height \textit{height} height 的长度。需要遍历数组 height \textit{height} height 两次分别计算数组 leftHeight \textit{leftHeight} leftHeight rightHeight \textit{rightHeight} rightHeight,以及遍历数组 height \textit{height} height 一次计算能接的雨水总量。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 height \textit{height} height 的长度。需要创建两个长度为 n n n 的数组 leftHeight \textit{leftHeight} leftHeight rightHeight \textit{rightHeight} rightHeight

解法二

思路和算法

对于下标 i i i,如果存在下标 prev \textit{prev} prev curr \textit{curr} curr,满足 prev < curr < i \textit{prev} < \textit{curr} < i prev<curr<i height [ prev ] \textit{height}[\textit{prev}] height[prev] height [ i ] \textit{height}[i] height[i] 都大于 height [ curr ] \textit{height}[\textit{curr}] height[curr],则存在一个能接雨水的区域。

由此可以使用单调栈计算能接的雨水总量。单调栈存储数组 height \textit{height} height 的下标,满足从栈底到栈顶的下标对应的元素单调递减。

从左到右遍历数组 height \textit{height} height,当遍历到下标 i i i 时,如果栈内至少有两个下标且栈顶下标对应的高度小于或等于当前高度(即 height [ i ] \textit{height}[i] height[i]),记栈顶下标为 curr \textit{curr} curr curr \textit{curr} curr 的下面一个下标是 prev \textit{prev} prev,其中 prev < curr < i \textit{prev} < \textit{curr} < i prev<curr<i,则得到一个能接雨水的区域,该区域的横向下标范围是 [ prev + 1 , i − 1 ] [\textit{prev} + 1, i - 1] [prev+1,i1],竖向高度范围是 [ height [ curr ] , min ⁡ ( height [ prev ] , height [ i ] ) ] [\textit{height}[\textit{curr}], \min(\textit{height}[\textit{prev}], \textit{height}[i])] [height[curr],min(height[prev],height[i])],因此该区域能接的雨水量是 ( i − prev − 1 ) × ( min ⁡ ( height [ prev ] , height [ i ] ) − height [ curr ] ) (i - \textit{prev} - 1) \times (\min(\textit{height}[\textit{prev}], \textit{height}[i]) - \textit{height}[\textit{curr}]) (iprev1)×(min(height[prev],height[i])height[curr])

由于栈顶下标为 curr \textit{curr} curr,其下面一个下标是 prev \textit{prev} prev,因此将 curr \textit{curr} curr 出栈之后的新栈顶下标为 prev \textit{prev} prev。根据 prev \textit{prev} prev curr \textit{curr} curr i i i 计算能接的雨水量之后, prev \textit{prev} prev 变成新的 curr \textit{curr} curr,重复上述操作,直到栈为空或者栈顶下标对应的高度大于当前高度。然后将 i i i 入栈,继续遍历后面的下标并计算能接的雨水量,遍历结束时即可得到能接的雨水总量。

下面是示例 1 使用单调栈计算能接的雨水总量的过程。

图 2

  1. 下标 0 0 0 处的元素是 0 0 0,将 0 0 0 入栈, stack = [ 0 : 0 ] \textit{stack} = [0:0] stack=[0:0],其中左边为栈底,右边为栈顶,栈内存储下标,为了方便阅读加上了下标对应的元素值。

  2. 下标 1 1 1 处的元素是 1 1 1,由于 0 < 1 0 < 1 0<1(根据上述逻辑,此处应该是 0 ≤ 1 0 \le 1 01,由于不相等因此写成小于号,下同),因此将 0 0 0 出栈,由于栈为空因此不计算能接的雨水量,将 1 1 1 入栈, stack = [ 1 : 1 ] \textit{stack} = [1:1] stack=[1:1]

  3. 下标 2 2 2 处的元素是 0 0 0,将 2 2 2 入栈, stack = [ 1 : 1 , 2 : 0 ] \textit{stack} = [1:1, 2:0] stack=[1:1,2:0]

  4. 下标 3 3 3 处的元素是 2 2 2,计算能接的雨水量。

    1. 由于 0 < 2 0 < 2 0<2,因此将 2 2 2 出栈, curr = 2 \textit{curr} = 2 curr=2 prev = 1 \textit{prev} = 1 prev=1 i = 3 i = 3 i=3,能接雨水的区域是图中的 A \text{A} A 区域,能接的雨水量是 ( 3 − 2 − 1 ) × ( 1 − 0 ) = 1 (3 - 2 - 1) \times (1 - 0) = 1 (321)×(10)=1 amount \textit{amount} amount 更新为 1 1 1

    2. 由于 1 < 2 1 < 2 1<2,因此将 1 1 1 出栈,由于栈为空因此不计算能接的雨水量,将 3 3 3 入栈, stack = [ 3 : 2 ] \textit{stack} = [3:2] stack=[3:2]

  5. 下标 4 4 4 处的元素是 1 1 1,将 4 4 4 入栈, stack = [ 3 : 2 , 4 : 1 ] \textit{stack} = [3:2, 4:1] stack=[3:2,4:1]

  6. 下标 5 5 5 处的元素是 0 0 0,将 5 5 5 入栈, stack = [ 3 : 2 , 4 : 1 , 5 : 0 ] \textit{stack} = [3:2, 4:1, 5:0] stack=[3:2,4:1,5:0]

  7. 下标 6 6 6 处的元素是 1 1 1,计算能接的雨水量。

    1. 由于 0 < 1 0 < 1 0<1,因此将 5 5 5 出栈, curr = 5 \textit{curr} = 5 curr=5 prev = 4 \textit{prev} = 4 prev=4 i = 6 i = 6 i=6,能接雨水的区域是图中的 B \text{B} B 区域,能接的雨水量是 ( 6 − 4 − 1 ) × ( 1 − 0 ) = 1 (6 - 4 - 1) \times (1 - 0) = 1 (641)×(10)=1 amount \textit{amount} amount 更新为 2 2 2

    2. 由于 1 = 1 1 = 1 1=1(根据上述逻辑,此处应该是 1 ≤ 1 1 \le 1 11,由于相等因此写成等号,下同),因此将 4 4 4 出栈, curr = 4 \textit{curr} = 4 curr=4 prev = 3 \textit{prev} = 3 prev=3 i = 6 i = 6 i=6,能接的雨水量是 ( 6 − 3 − 1 ) × ( 1 − 1 ) = 0 (6 - 3 - 1) \times (1 - 1) = 0 (631)×(11)=0 amount \textit{amount} amount 更新为 2 2 2

    3. 6 6 6 入栈, stack = [ 3 : 2 , 6 : 1 ] \textit{stack} = [3:2, 6:1] stack=[3:2,6:1]

  8. 下标 7 7 7 处的元素是 3 3 3,计算能接的雨水量。

    1. 由于 1 < 3 1 < 3 1<3,因此将 6 6 6 出栈, curr = 6 \textit{curr} = 6 curr=6 prev = 3 \textit{prev} = 3 prev=3 i = 7 i = 7 i=7,能接雨水的区域是图中的 C \text{C} C 区域,能接的雨水量是 ( 7 − 3 − 1 ) × ( 2 − 1 ) = 3 (7 - 3 - 1) \times (2 - 1) = 3 (731)×(21)=3 amount \textit{amount} amount 更新为 5 5 5

    2. 由于 2 < 3 2 < 3 2<3,因此将 3 3 3 出栈,由于栈为空因此不计算能接的雨水量,将 7 7 7 入栈, stack = [ 7 : 3 ] \textit{stack} = [7:3] stack=[7:3]

  9. 下标 8 8 8 处的元素是 2 2 2,将 8 8 8 入栈, stack = [ 7 : 3 , 8 : 2 ] \textit{stack} = [7:3, 8:2] stack=[7:3,8:2]

  10. 下标 9 9 9 处的元素是 1 1 1,将 9 9 9 入栈, stack = [ 7 : 3 , 8 : 2 , 9 : 1 ] \textit{stack} = [7:3, 8:2, 9:1] stack=[7:3,8:2,9:1]

  11. 下标 10 10 10 处的元素是 2 2 2,计算能接的雨水量。

    1. 由于 1 < 2 1 < 2 1<2,因此将 9 9 9 出栈, curr = 9 \textit{curr} = 9 curr=9 prev = 8 \textit{prev} = 8 prev=8 i = 10 i = 10 i=10,能接雨水的区域是图中的 D \text{D} D 区域,能接的雨水量是 ( 10 − 8 − 1 ) × ( 2 − 1 ) = 1 (10 - 8 - 1) \times (2 - 1) = 1 (1081)×(21)=1 amount \textit{amount} amount 更新为 6 6 6

    2. 由于 2 = 2 2 = 2 2=2,因此将 8 8 8 出栈, curr = 8 \textit{curr} = 8 curr=8 prev = 7 \textit{prev} = 7 prev=7 i = 10 i = 10 i=10,能接的雨水量是 ( 10 − 7 − 1 ) × ( 1 − 1 ) = 0 (10 - 7 - 1) \times (1 - 1) = 0 (1071)×(11)=0 amount \textit{amount} amount 更新为 6 6 6

    3. 10 10 10 入栈, stack = [ 7 : 3 , 10 : 2 ] \textit{stack} = [7:3, 10:2] stack=[7:3,10:2]

  12. 下标 11 11 11 处的元素是 1 1 1,将 11 11 11 入栈, stack = [ 7 : 3 , 10 : 2 , 11 : 1 ] \textit{stack} = [7:3, 10:2, 11:1] stack=[7:3,10:2,11:1]

能接的雨水总量 amount = 6 \textit{amount} = 6 amount=6

代码

class Solution {
    public int trap(int[] height) {
        int amount = 0;
        Deque<Integer> stack = new ArrayDeque<Integer>();
        int n = height.length;
        for (int i = 0; i < n; i++) {
            while (!stack.isEmpty() && height[stack.peek()] <= height[i]) {
                int curr = stack.pop();
                if (stack.isEmpty()) {
                    break;
                }
                int prev = stack.peek();
                int currWidth = i - prev - 1;
                int currHeight = Math.min(height[prev], height[i]) - height[curr];
                amount += currWidth * currHeight;
            }
            stack.push(i);
        }
        return amount;
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 height \textit{height} height 的长度。需要遍历数组 height \textit{height} height 一次,每个下标最多入栈和出栈各一次。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 height \textit{height} height 的长度。空间复杂度主要取决于栈空间,栈内元素个数不会超过 n n n

解法三

思路和算法

解法一使用两个数组 leftHeight \textit{leftHeight} leftHeight rightHeight \textit{rightHeight} rightHeight 存储每个下标左边和右边的最大高度,空间复杂度是 O ( n ) O(n) O(n)。可以使用双指针和两个变量代替两个数组,将空间复杂度降到 O ( 1 ) O(1) O(1)

创建两个变量 left \textit{left} left right \textit{right} right 表示左右指针,以及两个变量 leftHeight \textit{leftHeight} leftHeight rightHeight \textit{rightHeight} rightHeight 分别表示左指针和右指针遍历到的最大高度,初始时 left = 0 \textit{left} = 0 left=0 right = n − 1 \textit{right} = n - 1 right=n1 leftHeight = 0 \textit{leftHeight} = 0 leftHeight=0 rightHeight = 0 \textit{rightHeight} = 0 rightHeight=0

计算能接的雨水总量的做法如下:

  1. 使用 height [ left ] \textit{height}[\textit{left}] height[left] height [ right ] \textit{height}[\textit{right}] height[right] 分别更新 leftHeight \textit{leftHeight} leftHeight rightHeight \textit{rightHeight} rightHeight

  2. 比较 leftHeight \textit{leftHeight} leftHeight rightHeight \textit{rightHeight} rightHeight 的大小并更新能接的雨水总量:

    • 如果 leftHeight < rightHeight \textit{leftHeight} < \textit{rightHeight} leftHeight<rightHeight,则下标 left \textit{left} left 处的雨水量是 leftHeight − height [ left ] \textit{leftHeight} - \textit{height}[\textit{left}] leftHeightheight[left],将下标 left \textit{left} left 处的雨水量加到能接的雨水总量,然后将 left \textit{left} left 1 1 1

    • 如果 leftHeight ≥ rightHeight \textit{leftHeight} \ge \textit{rightHeight} leftHeightrightHeight,则下标 right \textit{right} right 处的雨水量是 rightHeight − height [ right ] \textit{rightHeight} - \textit{height}[\textit{right}] rightHeightheight[right],将下标 right \textit{right} right 处的雨水量加到能接的雨水总量,然后将 right \textit{right} right 1 1 1

  3. 重复第 1 步和第 2 步,直到 left = right \textit{left} = \textit{right} left=right,即两个指针相遇,此时结束计算,得到能接的雨水总量。

代码

class Solution {
    public int trap(int[] height) {
        int amount = 0;
        int left = 0, right = height.length - 1;
        int leftHeight = 0, rightHeight = 0;
        while (left < right) {
            leftHeight = Math.max(leftHeight, height[left]);
            rightHeight = Math.max(rightHeight, height[right]);
            if (leftHeight < rightHeight) {
                amount += leftHeight - height[left];
                left++;
            } else {
                amount += rightHeight - height[right];
                right--;
            }
        }
        return amount;
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 height \textit{height} height 的长度。两个指针的移动次数之和不超过 n n n

  • 空间复杂度: O ( 1 ) O(1) O(1)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

伟大的车尔尼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值