LeetCode刷题05-42. 接雨水 超详细五种解法!

这是字节跳动面试题库里面的题,也是上手做的第一道困难的题


题目

在这里插入图片描述

五种解法从此开始

一、一行一行算(也是我的方法)超时了可以不看

说实话,这道题看完一瞬间我就冒出了这个想法,并且五分钟写完代码(代码习惯不好,debug花了很久)当时我的心情是
在这里插入图片描述就这,困难题目不过如此,字节跳动不过如此

但是!一运行,超时!!!在这里插入图片描述
看题目下面的评论说这个方法本来是AC的,后来改成超时了,我吐了,因为思路清晰并且运行通过(纪念自己的努力),所以还是贴上来

思路

这种一行一行算思路很简单:相当于切片,一层一层算水坑有多少

先算第一层,
意思就是,高度大于等于1的墙围起来的范围,[0,1,0,2,1,0,1,3,2,1,2,1]的原数组如下,判断大于等于1的范围。
在这里插入图片描述
被提取成[1,0,2,1,0,1,3,2,1,2,1]
然后找出有几块高度小于1,显然第一层有两个小于1的,第一层能装两块水。
在这里插入图片描述

然后算第二层
高度大于等于2的墙围起来的范围找出来[2,1,0,1,3,2,1,2]如下
在这里插入图片描述
然后找红色框范围内高度小于2的,发现有四块,黄色圆圈标出来的。

就是这样的思想,一直使操作的层数递增到最高的墙。
第三层就他一堵墙,没有小于3的,所以没有能装水的块在这里插入图片描述
最后就是第一层的2块加第二层的4块=6

逐句注释的代码实现

class Solution {
    public int trap(int[] height) {

        //首先考虑特殊情况 数组长为0或者1
        if (height.length == 0 && height.length == 1) return 0;

        int totalWater = 0;//总水量 
        int level = 1;//起始层数
        int maxNum = 0;//最大值

        //取最大值
        for (int i : height) {
            maxNum = i >= maxNum ? i : maxNum;
        }
        
        while (level <= maxNum) {
            ArrayList<Integer> temArr = new ArrayList<>();

            //下面找到第一层 首先找到首尾  不小于1的第一个和最后一个数
            //新建一个arrlist存坐标 
            ArrayList<Integer> index = new ArrayList<>();
            for (int i = 0; i < height.length; i++) {
                if (height[i] >= level) {
                    index.add(i);
                }
            }

            //用首尾坐标切割原数组成新的数组
            // 原数组 [0,1,0,2,1,0,1,3,2,1,2,1]
            // 要切成 [  1,0,2,1,0,1,3,2,1,2,1]
            // 坐标数组                           index[1,3,4,6,7,8,9,10,11]
            // 坐标 最小的1,最大的11, 最小的排在 index里面的第一位 index.get(0)  最大的排在index里的最后一位 index.get(index.size()-1)

            int minINdex = Collections.min(index);
            int maxIndex = Collections.max(index);
            for (int i = index.get(0); i < index.get(index.size() - 1) + 1; i++) {
                temArr.add(height[i]);
            }
            minINdex = 0;
            maxIndex = 0;


            //然后从刚才找出的层里找比一小的 就是第一层能装的雨水坑
            for (int j = 0; j < temArr.size(); j++) {
                if (temArr.get(j) < level) {
                    totalWater++;
                }
            }

            temArr.clear();
            level++;
        }

        return totalWater;
    }
}

IDEA里运行是没问题的,但是提交就超时了
原因是leetcode后台用来测试的最后一个数组。。。四五页都没显示全,大概上千个元素?
时间复杂度:如果最大的数是 m,个数是 n,那么就是 O(m*n) 显然数组很长而且最大元素很大时候会超时
空间复杂度:O(1)
在这里插入图片描述

二、按照列求

思路

对每一列来说,都判断自己这一列能不能装水,如何判断呢?
看自己左边最高的墙(不包括自己)和右边最高的墙(不包括自己),根据短板效应取这俩中更小的那个值lowWall
lowWall比自己高那才可能出现凹坑才能存水对吧,旁边的比自己矮那不流出去了
然后计算这一列上存了多少水
首先两端的墙不用考虑,存不了,直接从第二个看
比如下面红色这一列能存几个水呢?

1.红色自己高度是1,左边最高是绿色,高度为0,右边最高是紫色,值为3,根据短板效应取lowWall = 0,
2.lowWall没有自己高,装不了

在这里插入图片描述

来看个能装的例子,下图我们来看红色框里这一列,它能装多少水呢?
首先,左边最高的墙是绿色,高度为2,右边最高的墙为紫色,高度为3,lowWall取值为2
lowWall大于红色自身高度1所以能装啊

在这里插入图片描述
那每列能装多少水呢?根据上图看出来,是(lowWall-自身高度)
最后按照这个方法计算每一列就完事了

逐句注释的代码实现

public int trap(int[] height) {
    int sum = 0;
    //最两端的列不用考虑,因为一定不会有水。所以下标从 1 到 length - 2
    for (int i = 1; i < height.length - 1; i++) {
        int max_left = 0;
        //找出左边最高
        for (int j = i - 1; j >= 0; j--) {
            if (height[j] > max_left) {
                max_left = height[j];
            }
        }
        int max_right = 0;
        //找出右边最高
        for (int j = i + 1; j < height.length; j++) {
            if (height[j] > max_right) {
                max_right = height[j];
            }
        }
        //找出两端较小的
        int min = Math.min(max_left, max_right);
        //只有较小的一段大于当前列的高度才会有水,其他情况不会有水
        if (min > height[i]) {
            sum = sum + (min - height[i]);
        }
    }
    return sum;
}


时间复杂度:O(n²),遍历每一列需要 nn,找出左边最高和右边最高的墙加起来刚好又是一个 n,所以是 n²。
空间复杂度:O(1)

三、动态规划(优化刚才的按列)

思路

刚才代码中出现了一些重复出现的循环遍历,每次计算新的一列时候,我们都要重新找两边最高的墙,完全没必要啊,利用动态规划的思想,把这些信息放到一个数组里就ok,动态规划的思想很复杂,在这里说不清,以后再详细讲解,反正大概就是把重复的方法归纳起来复用

逐句注释的代码实现

public int trap(int[] height) {
	//sum计算总和
	// 新建两个长度和height一样的数组用来存 每一列的左右最高墙在height中的索引
    int sum = 0;
    int[] max_left = new int[height.length];
    int[] max_right = new int[height.length];
    
    //求所有列的左边最高墙  索引都放在max_left里
    for (int i = 1; i < height.length - 1; i++) {
        max_left[i] = Math.max(max_left[i - 1], height[i - 1]);
    }

	//求所有列的右边最高墙  索引都放在max_right里
    for (int i = height.length - 2; i >= 0; i--) {
        max_right[i] = Math.max(max_right[i + 1], height[i + 1]);
    }

	//计算每一列的能存多少水累加起来   这一步和上面方法没区别
    for (int i = 1; i < height.length - 1; i++) {
        int min = Math.min(max_left[i], max_right[i]);
        if (min > height[i]) {
            sum = sum + (min - height[i]);
        }
    }
    return sum;
}


动态规划优化了求一侧最大值的操作,其余代码没变化
时间复杂度:O(n)
空间复杂度:O(n),用来保存每一列左边最高的墙和右边最高的墙。
相比刚才的方法,时间复杂度降了下来,空间复杂度略有上升

四、双指针(把刚才上升的空间复杂度给降下来成为指标最优的解法)

思路

这道题中,可以看到,max_left[ i ] 和 max_right[ i ] 数组中的元素我们其实只用一次,然后就再也不会用到了。所以我们可以不用数组,只用一个元素就行了。

逐句注释的代码实现

我们先改造下 max_left

public int trap(int[] height) {
    int sum = 0;
    int max_left = 0;

	//右侧的没改还用数组存
    int[] max_right = new int[height.length];
    for (int i = height.length - 2; i >= 0; i--) {
        max_right[i] = Math.max(max_right[i + 1], height[i + 1]);
    }

	//左侧的改了
    for (int i = 1; i < height.length - 1; i++) {
        max_left = Math.max(max_left, height[i - 1]);
        int min = Math.min(max_left, max_right[i]);
        if (min > height[i]) {
            sum = sum + (min - height[i]);
        }
    }
    return sum;
}


时间复杂度: O(n)
空间复杂度:O(1)
这种方法运行时间和内存消耗都击败了全国100%!
在这里插入图片描述

可以把右面也改一下,但是这样内存消耗居然只能击败全国97%,具体原因没思考,毕竟有个双百的了~

五、栈

思路

这道题好像就是想考栈= =
就是把墙的高度压入栈中,栈顶元素就是上一个墙的高度,如果指针指的当前墙比上一个墙高,那就说明,能存水,没有上一个墙高,那就存不了水,把这个墙放进去
具体思路都写代码注释里面了,注释已经不能再细了

逐句注释的代码实现

    public int trap(int[] height) {

        /**
         * 初始栈
         * 初始总和
         * 初始指针指向第一个元素
         */
        Stack<Integer> stack = new Stack<>();
        int sum = 0;
        int currIndex = 0;

        /**
         * 最后用大循环包围
         */
        while (currIndex < height.length) {
            /**
             * 如果下一个比栈顶的大,那就进行一系列操作
             */
            while (!stack.empty() && height[currIndex] > height[stack.peek()]) {
                /**
                 * 1.首先取出栈顶元素 是计算面积的底
                 * 2.然后刚才的底没用了,出栈,我们要他之前的那个墙(新的栈顶)的高度,和currIndex指针指向的墙比,哪个矮,哪个-底就等于计算面积的高
                 *                  (矮墙-底) X 底边宽 = 面积
                 * 3.底边宽怎么求?  底边宽 = 后面的墙-前面的墙-1
                 * 4.所以最终 sum = 面积 = (矮墙-底) X 底边宽
                 */

                //1.
                int bottomHeight = height[stack.peek()];

                //2.
                stack.pop();
                //这里注意 要是出栈以后 栈空了那就跳出循环进行下一个height里的元素操作
                if (stack.empty()) break;
                //go on
                int lowerWall = Math.min(height[currIndex], height[stack.peek()]);

                //3.
                int bottomWide = currIndex - stack.peek() - 1;

                //4.
                sum = sum + (lowerWall - bottomHeight) * bottomWide;


            }
            /**
             * 如果下一个比栈顶的小,那就直接入栈
             */
            stack.push(currIndex);


            /**
             * 遍历记得指针往后走
             */
            currIndex++;


        }


        return sum;
    }

}

时间复杂度:虽然 while 循环里套了一个 while 循环,但是考虑到每个元素最多访问两次,入栈一次和出栈一次,所以时间复杂度是 O(n)。
空间复杂度O(n)。栈的空间。
在这里插入图片描述
运行时间不算nice,这样看,双指针的动态规划牛🍺,各种优秀算法都有动态规划和双指针


一壶水,一包烟,一道力扣做一天
在这里插入图片描述

明天继续在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值