leetcode-接雨水

78 阅读4分钟

接雨水

leetcode链接

分析:

image.png

height = [0,1,0,2,1,0,1,3,2,1,2,1]

观察图片可以发现每一列能接多少水取决于:当前格子左边最高的和右边最高的其中较低的和当前格子的差值

例如:

第3个格子左边最大是第2个格子,右边最大是第8个格子,受限于左边的格子,所以只能存一格水

image.png

第5个格子左边最大是第4个格子,右边最大是第8个格子,受限于第4个格子,最高只能到2,但是自身只有1的高度,所以只能存1个格子的水

image.png

解法一 暴力:

第一层循环从1 - n-2,第二层循序遍历找左边最大的和右边最大的;找到每一列能存多少水,然后存起来:

class Solution {
    public int trap(int[] height) {
        int res = 0;
        for (int i = 1; i < height.length - 1; i++) {
            int lMax = 0;
            for (int j = 0; j < i; j++) {
                lMax = Math.max(lMax, height[j]);
            }
            int rMax = 0;
            for (int j = i + 1; j < height.length; j++) {
                rMax = Math.max(rMax, height[j]);
            }
            if (lMax > height[i] && rMax > height[i]) {
                res += Math.min(lMax, rMax) - height[i];
            }
        }
        return res;
    }
}

时间复杂度:o(n*n)

空间复杂度:o(1)

解法二 动态规划:

解法一中每查一个格子都需要遍历这个格子左边所有和右边所有的,有很多重复的查询,因此考虑采用动态规划,用空间换时间的方式,减少时间复杂度:

  1. 递推公式含义:n位置左边的最大值和右边的最大值
  2. 递推公式:Fn = max(Fn-1, height[i-1]);
  3. dp数组初始化:为 0 即可,因为0位置左边最大值是0,n-1位置右边最大值为0
  4. 遍历顺序:遍历两次,一次从左到右(1到n-2),一次从右到左(n-2 到 1)
  5. 举例:F1 = max(height[0], F0) = max(0, 0);
class Solution {
    public static int trap(int[]height) {
        int n = height.length;
        int[]leftMax = new int[n];
        int[]rightMax = new int[n];
        for (int i = 1; i < n - 1; i++) {
            leftMax[i] =Math.max(leftMax[i - 1], height[i - 1]);
        } for (int i = n - 2; i > 0; i--) {
            rightMax[i] =Math.max(rightMax[i + 1], height[i + 1]);
        } int sum = 0;
        for (int i = 1; i < n - 1; i++) {
            int v = Math.min(leftMax[i], rightMax[i])-height[i];
            sum += Math.max(v, 0);
        } return sum;
    }
}

时间复杂度:o(n)

空间复杂度:o(n)

解法三 双指针:

一个格子能存多少水取决于它左右矮的一方。

上面动态规划的方法需要使用n的空间保持左边和右边的最大值,但是实际上数组中的每一个格子都只使用了一次,并且格子能存多少水实际是取决于左右最高比较矮的一个,那么就可以使用双指针的方式,一个指针从0开始,一个从n-1开始,定义lmax为左边的最大值,rmax 为右边最大值,移动规则是高的不动,低的移动:

lmax > rmax 的时候说明左边比右边高,可以得知j左边的最大值一定是大于j右边的最大值的,这时候我们可以j这个格子能存多少水,然后j左移一位

lmax < rmax 的时候说明左边比右边矮,可以得知i右边的最大值一定是大于i左边的最大值的,这时候我们可以i这个格子能存多少水,然后i右移一位

image.png

image.png

image.png

image.png

这个时候我们发现lmax和rmax是相等的,由此可以推断一个结论:i的右边的最大值肯定是大于等于i左边的;或者j左边的最大值一定是大于等于i右边的,因此可以将i能存的水位加上,i右移一位,或者加上j的水位,j左移一位

image.png

image.png 以此类推

代码:

class Solution {
    public static int trap(int[]height) {
        int n = height.length;
        int i = 0, j = n - 1;
        int lmax = 0, rmax = 0;
        int sum = 0;
        while (i < j) {
            lmax = Math.max(lmax, height[i]);
            rmax = Math.max(rmax, height[j]);
            if (lmax >= rmax) {
                sum += Math.max(0, rmax - height[j]);
                j--;
            } else {
                sum += Math.max(0, lmax - height[i]);
                i++;
            }
        } return sum;
    }
}

解法四 单调栈:

前面的三种解法我们都是通过求每一列能存多少水,那么是否可以求每一行能存多少水呢?

image.png

观察规律可以发现:当后面的柱子比当前柱子高的时候,就可以求一行的积水,例如:

这时候能存多少水,就取决于长*宽

image.png

image.png

image.png

这时候我们就可以采用一个栈的数据结构,当栈顶元素比当前下标元素高的时候,入栈,否则取出栈顶元素,那栈顶元素的前一个下标和当前下标的距离表示框,高就是栈顶元素与当前元素的高度差

代码:

class Solution {
    public int trap(int[]height) {
        if (null == height || 0 == height.length) {
            return 0;
        }
        Deque<Integer> deque = new ArrayDeque<>();
        int sum = 0;
        int i = 0;
        while (i < height.length) {
            if (deque.isEmpty()) {
                deque.addLast(i);
            } else if (height[deque.peekLast()] >height[i]){
                deque.addLast(i);
            } else if (height[deque.peekLast()] <height[i]){
                int mid = deque.pollLast();
                if (!deque.isEmpty()) {
                    sum += (Math.min(height[deque.peekLast()], height[i])-height[mid]) *(i - deque.peekLast() - 1);
                    continue;
                } else {
                    deque.addLast(i);
                }
            } else{
                deque.pollLast();
                deque.add(i);
            }
            i++;
        } return sum;
    }
}