首页 > 其他分享 >LeetCode42(接雨水)[三种解法:理解动态规划,双指针,单调栈]

LeetCode42(接雨水)[三种解法:理解动态规划,双指针,单调栈]

时间:2024-07-08 20:56:52浏览次数:19  
标签:leftMax int res 最大值 height LeetCode42 我们 解法 指针

接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
在这里插入图片描述
在这里插入图片描述
这是一道困难题,难度确实有点层次.我们先来朴素思想走一波.
要求能接多少雨水,我们可以具化到每个硅谷,每个硅谷能存多少雨水,那么答案就是每个硅谷的雨水所加之和.
对于每一个高度的柱子,我们要求出它的积水量,是等于它左边高度的最大值与右边高度的最大值中这两个值其中的小值减去当前硅谷的高度.
公式为:
在这里插入图片描述在这里插入图片描述

怎么理解这句话呢?为什么不是这个硅谷两旁的高度相比较较小值减去当前硅谷的高度,而是其左右两边的最大值呢.

对于这一小块,我们观察到积水处的左右两边好像跟我们拿其左右两边最大值与它身边两个的最小值所取到的积水处的值是一样的.
在这里插入图片描述

我们仔细来看看中间那部分.
在这里插入图片描述
这一部分如果取两边的值,我们将会漏掉上方那一个单位的正方形值,所以对于积水处的两旁界限我们应该是选取左右两边的最大值.

而为什么又要减去积水处的高度呢?我们再来看下面这一部分
在这里插入图片描述
其实不用我解释,现在看这幅图大家都能理解啦,我们肯定是需要减去它的基础高度值,才能求得实际上空的空间.也就是硅谷的面积.

所以基于这种求值的思路,我们开始来正式解题.

暴力法解题

我们谈到是要取一个硅谷点的左右两边最大值来求值.那么每当我们到达一个结点处,遍历它的左右两边找到其左右的最大值就可以完成这一步骤的计算.但由于每一个结点我们都需要遍历一遍数组,所以时间复杂度为O(²)
我相信大家应该都可以基于暴力能自主完成,这里不做代码解释,下面才是算法重点.

动态规划

我们谈到一个节点的左右两边的最大值.我们可不可以在计算之前,统计好每一个结点的左右两边的最大值.
也就是从左往右开始遍历,我们可以求得每个结点右边的最大值.
rightMax[i]=max(rightMax[i+1],height[i])
同理,从右往左遍历,我们可以求得每个结点左边的最大值.
leftMax[i]=max(leftMax[i−1],height[i])
总之就是在遍历计算前,我们打表把所有每个结点的左右两边的最大值存储好,之后我们要求时直接从打表过后的数组里面取就可
代码为

public int dpMethod(int[] height){
        int[] leftdp = new int[height.length];
        int[] rightdp = new int[height.length];
        int leftMax = 0;
        int rifhtMax = 0;
        int res = 0;
        for(int i = 0;i < height.length;i++){
            leftdp[i] = leftMax = Math.max(height[i],leftMax);
        }
        for(int i = height.length-1;i >= 0;i--){
            rightdp[i] = rifhtMax = Math.max(height[i],rifhtMax);
        }
        for(int i = 0;i < height.length;i++){
            res += Math.min(leftdp[i],rightdp[i]) - height[i]; 
        }
        return res;
    }

时间复杂度为O(n)

单调栈

我们发现硅谷处其实也就是发生破坏一个柱子的单调性时,产生了硅谷.我们可以利用这样一个特性完成题目的解题.对于每一个结点的索引,我们存放于栈中,每当这个结点的高度小于栈顶元素的值(也就是需要循环遍历),我们就将其索引值放于栈中.而遇到破坏单调性,也就是一个柱子的高度大于我们的栈顶元素时.我们将栈顶元素弹出,求得此时硅谷处的值.
公式也就是
res += Min(height[peek],height[i])-height[pop]
在这里插入图片描述
需要注意的是

我们应该在栈中无元素时,不用再进行求值,因为此时说明是边界情况,对应此时红框中的情况,当我们计算完pop处之后,下一次循环,我们将弹出peek处的元素,此时它的左边没有元素,也就是对应着此时栈中没有元素.我们不需要再进行求值.

还有一点不同的是,我们遍历处的height[j] 与我们的栈顶元素是有一段宽度的,我们计算面积应该带上宽度的乘积,及宽度长度为 i - peek - 1,对应的情况为
在这里插入图片描述

代码为

public int stack(int[] height){
        LinkedList<Integer> rain = new LinkedList();
        int res = 0;
        for(int i = 0;i < height.length;i++){
            while(!rain.isEmpty() && height[rain.peek()] < height[i]){
                int pop = rain.pop();//弹出栈顶元素
                if(rain.isEmpty()){
                    break;
                }
                int left = rain.peek();//获取栈顶元素的值,还在栈中没有弹出
                int h = Math.min(height[i],height[left]) - height[pop];
                res += h * (i - left - 1);
            }
            rain.push(i);
        }
        return res;
    }

时间复杂度为O(n).

双指针

最后一种解法就是我们的双指针啦,也是最快的解法.不需要开辟任何空间,只需要常量级别的空间,而且只需要一次遍历即可完成.

注意到下标 i 处能接的雨水量由 leftMax[i] 和 rightMax[i] 中的最小值决定。由于数组 leftMax 是从左往右计算,数组 rightMax 是从右往左计算,因此可以使用双指针和两个变量代替两个数组。
遍历过程中,我们更新左右两端的最大值.
当左边的值小于右边的值时,我们直接拿着左边的最大值减去当前结点的高度即可.欸?为什么这里我们不需要再次比较左右两端的最大值,选取其中的较小值呢?
注意啦,我们先判断左边的元素是否大于右边的元素,如果大于我们挪动的是右指针,也就是说明如果右边的值没有大于过左边的值,将一直挪动的是右指针,间接性的把左右两端的最大值作了比较.
右边的值小于左边的值是也是如此.

代码为所以

public int trap(int[] height) {
        int left = 0;
        int right = height.length - 1;
        int leftMax = 0;
        int rightMax = 0;
        int res = 0;
        while(left < right){
            leftMax = Math.max(leftMax,height[left]);
            rightMax = Math.max(rightMax,height[right]);
            if(height[left] < height[right]){
                res += leftMax - height[left];
                left++;
            }else{
                res += rightMax - height[right];
                right--;
            }
        }
        return res;
    }

时间复杂度为O(n),空间复杂度为O(1)

标签:leftMax,int,res,最大值,height,LeetCode42,我们,解法,指针
From: https://blog.csdn.net/m0_65013257/article/details/140277372

相关文章

  • 大话C语言:第29篇 指针
    1指针概念指针:地址的变量化形式,其存储的是内存中某个存储单元的地址。它是地址的数值表示。指针变量:一种特殊的变量,它专门用于存放变量的地址(即指针)。注意,指针和指针变量的区别:指针本身是一个地址,这个地址指向内存中的一个存储单元。它只是一个内存地址的抽象表示,没......
  • C语言 指针和数组——指针数组的应用:命令行参数
    目录命令行参数演示命令行参数与main函数形参间的关系命令行参数什么是命令行参数(CommandLineArguments)?GUI界面之前,计算机的操作界面都是字符式的命令行界面(DOS、UNIX、Linux)例如,在DOS下拷贝文件用copyfile1.cfile2.c不......
  • 快慢指针 “体育中考”版
    你好,这是我的第一篇博客,写博客是为了梳理知识,同时帮助大家,以下有问题请提出来,要喷的话请指出喷的点。共进,共勉!请大家回忆体育中考惨痛的历史,那么直接开启故事。故事  由于四五月份的到来,我们也迎来了体育中考,其中最害怕的是1000米—300米跑道。体育中考当天,我和我的同......
  • / 用上指针 ,定义函数实现:终端输入 add + sub - mul * div / 执行 两个数 的加减乘除
    #include<stdio.h>#include<string.h>intmy_add(intdata1,intdata2){  returndata1+data2;}intmy_sub(intdata1,intdata2){  returndata1-data2;}intmy_mul(intdata1,intdata2){  returndata1*data2;}intmy_di......
  • 虚表和虚表指针 详解
    ......
  • Leetcode刷题记录1 哈希+双指针+滑动窗口
    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录前言hot1001.哈希#1.两数之和#49.字母异位词分组#128.最长连续序列2.双指针#283.移动零#11.盛最多水的容器#15.三数之和#42.接雨水3.滑动窗口#3.无重复字符的最长子串#438.找到字符串中所有......
  • C语言指针
    主要内容地址和指针变量的指针和指向变量的指针变量数组的指针和指向数组的指针变量字符串的指针和指向字符串的指针变量函数的指针和指向函数的指针变量★返回指针值的函数指针数组和指向指针的指针地址和指针在计算机中所有数据都存放在存储器中。把主存储器中的一个字......
  • Go语言--复合类型之指针与数组
    分类指针指针是一个代表着某个内存地址的值。这个内存地址往往是在内存中存储的另一个变量的值的起始位置。Go语言对指针的支持介于Java语言和C/C++语言之间,它既没有想Java语言那样取消了代码对指针的直接操作的能力,也避免了C/C++语言中由于对指针的滥用而造成......
  • 【LeetCode 0141】【链表】【双指针之快慢指针】判断给定单链表是否存在环
    LinkedListCycleGivenhead,theheadofalinkedlist,determineifthelinkedlisthasacycleinit.Thereisacycleinalinkedlistifthereissomenodeinthelistthatcanbereachedagainbycontinuouslyfollowingthe next pointer.Internal......
  • 关于指针
    指针是什么一个指针是一个地址是个保存内存地址的整数,对指针类型的定义不会影响变量的内存,但对内存的操作有用内存是个线性的线,周围有很多屋子,每个屋子都一个地址占一个字节,指针就是指这些地址。注意事项int*point=0//这种写法是无效指针可以替换为int*point=NULL;具体使用......