7.接雨水

96 阅读3分钟

题目链接

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

示例 1:

image.png

输入: 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

题解1 暴力解法

思路

柱子与柱子之间能接多少水,就像上一题一样,取决于两边的高度。上一题是木板,这一题是柱子。

还和柱子本身的高度有关系,如果两边柱子之间的最矮那一边比柱子本身要高,那才会形成一个洼地,能够留住雨水。如果比柱子本身要矮,那么不会留下雨水。

我们可以在遍历的时候先找到他左边和右边最高的柱子,然后和柱子本身高度做比较。如果为正则累加到结果中。

为什么要找最高而不是简单看左右两边?因为可能形成横向的积水。

image.png

图中高度为 0 的柱子左右两边是 1,而形成了高度为 2的积水,这都是因为左右两侧更高的柱子形成了更高的雨水拦截。

代码

function trap(height: number[]): number {
    const n: number = height.length;
    let result: number = 0;

    for (let i = 0; i < n; i++) {
        let left_max: number = 0;
        let right_max: number = 0;

        for (let j = 0; j < i; j++) {
            left_max = Math.max(left_max, height[j]);
        }

        for (let j = i + 1; j < n; j++) {
            right_max = Math.max(right_max, height[j]);
        }

        const water: number = Math.min(left_max, right_max) - height[i];
        if (water > 0) {
            result += water;
        }
    }

    return result;
};

时空复杂度分析

时间复杂度:两层循环,内部的两次循环只遍历了 n - 1次,所以渐进复杂度是 O(n^2)

空间复杂度:没有使用额外变量 O(1),内部两个 max 也是常数级

题解2 两次遍历

思路

有了暴力解法之后,我们很容易想到优化,就是把 left_maxright_max 缓存下来,不用每次都要在循环内部遍历计算。利用空间来换时间。

left_maxright_max 缓存为数组,最后计算总和即可。

代码

function trap(height: number[]): number {
    const n: number = height.length;
    const left_max: number[] = new Array(n).fill(0);
    left_max[0] = height[0];
    for (let i = 1; i < n; i++) {
        left_max[i] = Math.max(height[i], left_max[i - 1]);
    }

    const right_max: number[] = new Array(n).fill(0);
    right_max[n - 1] = height[n - 1];
    for (let i = n - 2; i >= 0; i--) {
        right_max[i] = Math.max(height[i], right_max[i + 1]);
    }

    let result: number = 0;
    for (let i = 0; i < n; i++) {
        // 为什么不用判断积水是否大于 `0`?
        // 因为在遍历的过程中,包含该柱子的高度,左右最矮也是自己
        // 即 Math.min(left_max[i], right_max[i]) >= height[i]
        result += Math.min(left_max[i], right_max[i]) - height[i];
    }
    return result;
};

时空复杂度分析

时间复杂度:3次循环,每次 O(n), 所以总的时间复杂度为 O(n)

空间复杂度:使用了2个额外数组去保存,空间复杂度开销也是 O(n)

题解3 双指针解法

上面的代码已经在时间复杂度上寻得最优解,现在还剩下优化空间复杂度。我们可以考虑一下,只在计算积水量的时候需要用到 left_maxright_max

是不是我们可以在遍历的过程中更新 left_maxright_max 并计算积水量呢?

答案是可以的。

代码

function trap(height: number[]): number {
    let left: number = 0;
    let right: number = height.length - 1;
    let left_max: number = 0;
    let right_max: number = 0;
    let result: number = 0;

    while (left < right) {
        if (height[left] < height[right]) {
            // 如果当前柱子高度小于等于left_max,累加水量;否则更新left_max
            if (height[left] >= left_max) {
                left_max = height[left];
            } else {
                result += left_max - height[left];
            }
            left += 1;
        } else {
            // 如果当前柱子高度小于等于right_max,累加水量;否则更新right_max
            if (height[right] >= right_max) {
                right_max = height[right];
            } else {
                result += right_max - height[right];
            }
            right -= 1;
        }
    }

    return result;
};

时空复杂度分析

时间复杂度:O(n)

空间复杂度:O(1)