Hard | LeetCode 42. 接雨水 | 单调栈 | 双指针

42. 接雨水

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

示例 1:

img
输入:height = [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 个单位的雨水(蓝色部分表示雨水)。 

示例 2:

输入:height = [4,2,0,3,2,5]
输出:9

提示:

  • n == height.length
  • 0 <= n <= 3 * 104
  • 0 <= height[i] <= 105

解题思路

方法一:暴力

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

时间复杂度: O(N ^ 2) 空间复杂度: O(1)

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++) { //Search the right part for max bar size
            max_right = Math.max(max_right, height[j]);
        }
        // 当前柱子能接的雨水量就是(左边最大值, 右边最大值)的较小者 减去 当前柱子的高度
        ans += Math.min(max_left, max_right) - height[i];
    }
    return ans;
}

方法二:更好的暴力

在方法一的基础上, 利用动态规划优化左边的最大值和右边的最大值的计算方法。

时间复杂度: O(N ^ 2) 空间复杂度: O(N)

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];
    int[] right_max = new int[size];
    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;
}

方法三: 单调递减栈

只有在高度下降的时候形成一个低洼, 这样等到高度上升的时候, 形成一个凹槽。然后将栈中比当前元素小的值出栈, 并且这些出栈的储水量是已经可以明确了, 就是这个元素的值。因为是较小者决定了能够储蓄水的高度。

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;
}

方法四: 双指针法

所以我们可以认为如果一端有更高的条形块(例如右端),积水的高度依赖于当前方向的高度(从左到右)。 当我们发现另一侧(右侧)的条形块高度不是最高的,我们则开始从相反的方向遍历(从右到左)。 在遍历时维护 left_max 和 right_max ,但是我们现在可以使用两个指针交替进行,实现 1 次遍历即可完成。

双指针移动时, 每次一定都是指针小的那个移动, 因为指针大的移动没有意义了。

如果指针移到了更大的位置, 宽度减小了, 高度还是不变。移到了更小的位置, 则宽度减小了, 高度也减小了, 得到了一个更小的结果, 没有意义。所以每次都是指针小的指向对方方向移动。

时间复杂度: O(N) 空间复杂度: O(1)

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) {
        // left指针值小于right指针值, 说明当前应该处理左指针, 因为此时存水的高度由左指针决定
        if (height[left] < height[right]) {
            if (height[left] >= left_max) {
                // 左边没有比当前更大的值, 说明此left不是一块洼地, 不能存水
                left_max = height[left];
            } else {
                // height[left] < left_max 说明此地方是一块洼地, 可以存水
                // 并且存水的高度就是洼地的高度
                ans += (left_max - height[left]);
            }
            ++left;
        } else {
            //  此else分支说明当前存水的高度由右指针决定
            if (height[right] >= right_max) {
                // 右边没有比当前更大的值,说明当前right位置, 不能存水
                right_max = height[right];
            } else {
                ans += (right_max - height[right]);
            }
            --right;
        }
    }
    return ans;
}
原文地址:https://www.cnblogs.com/chenrj97/p/14623396.html