• 单调栈题目:接雨水


    题目

    标题和出处

    标题:接雨水

    出处: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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    复杂度分析

    • 时间复杂度: 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    复杂度分析

    • 时间复杂度: 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    复杂度分析

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

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

  • 相关阅读:
    LeetCode用数组建立二叉树
    yum的nginx平滑升级
    【Interconnection Networks 互连网络】Dragonfly Topology 蜻蜓网络拓扑
    JVM年轻代(young generation)老年代(old generation tenured)持久代(permanent generation)GC
    【PCIE709-F】基于复旦微JFM7VX690T80 FPGA的全国产化8通道光纤双FMC接口数据处理平台
    Vue 之 provide和inject的使用
    vulnhub靶机Thoth-Tech
    关于融合软件运行unity程序被闪退解决方案
    有序序列判断
    C++语言之虚函数、多态、抽象类
  • 原文地址:https://blog.csdn.net/stormsunshine/article/details/121130293