【LeetCode-42】接雨水

4.6 接雨水【42】

4.6.1 题目描述

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

4.6.2 方法一:暴力

直观想法

直接按问题描述进行。对于数组中的每个元素,我们找出下雨后水能达到的最高位置,等于两边最大高度的较小值减去当前高度的值。

算法

  • 初始化 ans=0
  • 从左向右扫描数组:
    • 初始化 KaTeX parse error: Expected 'EOF', got '_' at position 10: \text{max_̲left}=0 和 max_right=0
    • 从当前元素向左扫描并更新:
      • KaTeX parse error: Expected 'EOF', got '_' at position 10: \text{max_̲left}=\max(\tex…
    • 从当前元素向右扫描并更新:
      • KaTeX parse error: Expected 'EOF', got '_' at position 10: \text{max_̲right}=\max(\te…
    • KaTeX parse error: Expected 'EOF', got '_' at position 15: \min(\text{max_̲left},\text{max… 累加到 ans
public int trap(int[] height) {
    int ans = 0;
    int size = height.length;
    for (int i = 1; i < size - 1; i++) {	// 左右边界两条柱不可能蓄水,只能是中间条柱可以蓄水
        int max_left = 0, max_right = 0;
        for (int j = i; j >= 0; j--) { //查找左边最高的条形柱大小
            max_left = Math.max(max_left, height[j]);
        }
        for (int j = i; j < size; j++) { //查找右边最高的条形柱大小
            max_right = Math.max(max_right, height[j]);
        }
        ans += Math.min(max_left, max_right) - height[i];
    }
    return ans;
}
// 通过计算每一个柱形条上的蓄水量累加来得到答案

复杂性分析

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)。数组中的每个元素都需要向左向右扫描。
  • 空间复杂度 O(1) 的额外空间。

4.6.3 方法二:动态规划

直观想法

在暴力方法中,我们仅仅为了找到最大值每次都要向左和向右扫描一次。但是我们可以提前存储这个值。因此,可以通过动态规划解决。

这个概念可以见下图解释:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9i9xJKlO-1657953094135)(.assets/image-20220715160235232.png)]

算法

  • 找到数组中从下标 i 到最左端最高的条形块高度 left_max。
  • 找到数组中从下标 i 到最右端最高的条形块高度 right_max。
  • 扫描数组 height 并更新答案:
    • 累加 KaTeX parse error: Expected 'EOF', got '_' at position 15: \min(\text{max_̲left}[i],\text{… 到 ans 上
public int trap(int[] height) {
    if (height == null || height.length == 0)
        return 0;
    int ans = 0;
    int size = height.length;
    int[] left_max = new int[size];		// 用于将i位置左边最高柱形条值储存起来,不用每次都求值
    int[] right_max = new int[size];	// 用于将i位置左边最高柱形条值储存起来,不用每次都求值
    left_max[0] = height[0];
    for (int i = 1; i < size; i++) {
        left_max[i] = Math.max(height[i], left_max[i - 1]);
    }
    right_max[size - 1] = height[size - 1];
    for (int i = size - 2; i >= 0; i--) {
        right_max[i] = Math.max(height[i], right_max[i + 1]);
    }
    for (int i = 1; i < size - 1; i++) {
        ans += Math.min(left_max[i], right_max[i]) - height[i];
    }
    return ans;
}
// 通过计算每一个柱形条上的蓄水量累加来得到答案

复杂性分析

  • 时间复杂度:O(n)。
    • 存储最大高度数组,需要两次遍历,每次 O(n) 。
    • 最终使用存储的数据更新 ans ,O(n)。
  • 空间复杂度:O(n) 额外空间。
    • 和方法 1 相比使用了额外的 O(n) 空间用来放置 left_max 和 right_max 数组。

4.6.4 方法三:栈的应用

直观想法

我们可以不用像方法 2 那样存储最大高度,而是用栈来跟踪可能储水的最长的条形块。使用栈就可以在一次遍历内完成计算。

我们在遍历数组时维护一个栈。如果当前的条形块小于或等于栈顶的条形块,我们将条形块的索引入栈,意思是当前的条形块被栈中的前一个条形块界定。如果我们发现一个条形块长于栈顶,我们可以确定栈顶的条形块被当前条形块和栈的前一个条形块界定,因此我们可以弹出栈顶元素并且累加答案到 ans 。

算法

  • 使用栈来存储条形块的索引下标。
  • 遍历数组:
    • 当栈非空且 \text{height}[current]>\text{height}[st.top()]height[current]>height[st.top()]
      • 意味着栈中元素可以被弹出。弹出栈顶元素 top。
      • 计算当前元素和栈顶元素的距离,准备进行填充操作
        distance = current − st.top ( ) − 1 \text{distance} = \text{current} - \text{st.top}() - 1 distance=currentst.top()1
      • 找出界定高度
        KaTeX parse error: Expected 'EOF', got '_' at position 14: \text{bounded_̲height} = \min(…
      • 往答案中累加积水量 KaTeX parse error: Expected 'EOF', got '_' at position 61: …s \text{bounded_̲height}
    • 将当前索引下标入栈
    • 将 current 移动到下个位置
public int trap(int[] height) {
    int ans = 0, current = 0;
    Deque<Integer> stack = new LinkedList<Integer>();
    while (current < height.length) {
        while (!stack.isEmpty() && height[current] > height[stack.peek()]) {
            int top = stack.pop();
            if (stack.isEmpty())
                break;
            int distance = current - stack.peek() - 1;
            int bounded_height = Math.min(height[current], height[stack.peek()]) - height[top];
            ans += distance * bounded_height;
        }
        stack.push(current++);
    }
    return ans;
}
// 通过计算两个柱形条直接的蓄水量累加得到答案

复杂性分析

  • 时间复杂度:O(n)。
    • 单次遍历 O(n) ,每个条形块最多访问两次(由于栈的弹入和弹出),并且弹入和弹出栈都是 O(1) 的。
  • 空间复杂度:O(n)。 栈最多在阶梯型或平坦型条形块结构中占用 O(n) 的空间。

4.6.5 方法四:使用双指针

直观想法

和方法 2 相比,我们不从左和从右分开计算,我们想办法一次完成遍历。
从动态规划方法的示意图中我们注意到,只要 right_max[i] > left_max[i] (元素 0 到元素 6),积水高度将由 left_max 决定,类似地 left_max[i]>right_max[i](元素 8 到元素 11)。
所以我们可以认为如果一端有更高的条形块(例如右端),积水的高度依赖于当前方向的高度(从左到右)。当我们发现另一侧(右侧)的条形块高度不是最高的,我们则开始从相反的方向遍历(从右到左)。
我们必须在遍历时维护 left_max 和 right_max ,但是我们现在可以使用两个指针交替进行,实现 1 次遍历即可完成。

算法(可参考LeetCode得动画,非常形象)
在这里插入图片描述

public int trap(int[] height) {
    int left = 0, right = height.length - 1;
    int ans = 0;
    int left_max = 0, right_max = 0;
    while (left < right) {
        if (height[left] < height[right]) {		// 哪边的柱子低,蓄水量就依赖哪一边
            if (height[left] >= left_max) {
                left_max = height[left];
            } else {
                ans += (left_max - height[left]);
            }
            ++left;
        } else {
            if (height[right] >= right_max) {
                right_max = height[right];
            } else {
                ans += (right_max - height[right]);
            }
            --right;
        }
    }
    return ans;
}

复杂性分析

  • 时间复杂度:O(n)。单次遍历的时间O(n)。
  • 空间复杂度:O(1) 的额外空间。left, right, left_max 和 right_max 只需要常数的空间。

4.6.6 my answer—栈

class Solution {
    public int trap(int[] height) {
        int ans = 0;
        int cur = 0;
        Deque<Integer> stack = new ArrayDeque<>();
        while(cur < height.length){
            while(!stack.isEmpty() && height[cur] > height[stack.peek()]){	// 栈非空,且当前值比栈顶值大
                int top = stack.pop();	
                if(stack.isEmpty()){
                    break;
                }
                int left = stack.peek();
                int curWith = cur - left -1;
                //高度取两者较低的,还需减去中间已经计算过的高度
                int curHeight = Math.min(height[cur],height[left]) - height[top]; 
                ans +=  curWith*curHeight;           
            }
            stack.push(cur++);
        }
        return ans;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值