最详细最通俗易懂的LeetCode 第42题《接雨水》完整讲解加代码

最详细最通俗易懂的LeetCode 第42题《接雨水》完整讲解加代码

1.分析题目

标题

        我滴妈《困难》题目走了走了。好多人看到就退缩了,我一开始也是,一看到这种就不敢做。但是当你认真看过思考过题目你就发现其实和简单题目的逻辑八九不离十只不过多了几个步骤而已。所以不要怕,一看题目接雨水,小孩子来都知道接的蓝色格子有6个,难的不过就是想怎么去计算更省事儿罢了。

题干理解

        题目给定我们一个非负整数的数组,代表存在0和正数正数(0代表不存在柱子,正数就代表对应高度的柱子)。柱子的高度由数组中数字的大小决定的(例如元素值为2,柱子的高度就是2),题目给了我们图就很好理解了,不就是计算两个柱子中间夹了多少水(蓝格子)吗。没有其他东西了。

解题思路

        首先我们要有一点生活常识:如果给你一个长方体的盆,左边高为3cm右边高为5cm,如图

那我接水不管怎么样最高肯定就接3cm高的水,再高就会溢出来。那我们是怎么得到这个3cm的接水极限高度呢。是不是首先需要比较两边的高度谁更低,拿低的那一边的高度减去盆底得到我的极限高度。(3<5 , 3-0 = 3cm)  所以解这道题的关键就是:一个位置能接多少水,取决于它左边最高的柱子和右边最高的柱子中更矮的那个,减去当前柱子的高度。我们现在知道了如何解题,现在就是去想如何实现这个步骤。下面我将用4个方法带你完成这道题。

2.代码讲解

(1)嵌套for循环:暴力解法

class Solution { public int trap(int[] height) { if(height == null || height.length <= 2) return 0; int total = 0; for(int i = 1 ; i < height.length - 1 ; i++){ int leftmax = 0; for(int j = 0 ; j < i ; j++){ leftmax = Math.max(leftmax , height[j]); } int rightmax = 0; for(int j = i + 1 ; j < height.length ; j++){ rightmax = Math.max(rightmax , height[j]); } int water = Math.min(leftmax , rightmax) - height[i]; total += Math.max(water , 0); } return total; } }

如果你不是天才,学习就应该走一种循序渐进的道路。我们最开始不追求最完美的方法,先确保自己能做出来,哪怕时间复杂度什么的都过不了。嵌套循环就是最好的起手方法。

我们把代码拆解一下,不然看起来太吓人了,乱七八糟一大团杂在这。直接劝退哈哈哈

if(height == null || height.length <= 2) return 0; int total = 0;

第一行这个操作:是对height数组进行一个预处理提前检查数组是否为空或数组长度小于等于2。减少后续不必要的时间损耗。

为什么是不能小于等于2个元素,因为只有四种情况

1.左边为0,右边有1个柱子----->水从左边流走

2.右边为0,左边有1个柱子----->水从右边流走

3.左边右边都有柱子-------->水直接从中间流到两边

4.没有柱子,两个元素都为0--------->水直接流走

第二行这个操作:定义一个total用来记录接水的数量

for(int i = 1 ; i < height.length - 1 ; i++)

这里的i是我每次遍历的当前元素,那为什么我的起点是1,终点是height.length - 1  特别简单。就因为第一位和最后一位接不住水,旁边都没柱子怎么接对吧。所以这里限制范围减少不必要的计算。

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

这里是记录当前元素左边最高的柱子高度为多少。Math.max意思就是括号里两个数字谁更大,这个Math推荐学习一下,很实用。下面我们用图来演示一下过程

leftmax = 0 ; height[0] = 0 ;leftmax = Math.max = 0

leftmax = 0 ; height[1] = 1 ;leftmax = Math.max = 1

leftmax = 1 ; height[2] = 0 ;leftmax = Math.max = 1

leftmax = 1 ; height[3] = 2 ;leftmax = Math.max = 2

leftmax = 2 ; height[4] = 1 ;leftmax = Math.max = 2

leftmax = 2 ; height[5] = 0 ;leftmax = Math.max = 2

leftmax = 2 ; height[6] = 1 ;leftmax = Math.max = 2

leftmax = 2 ; height[7] = 3 ;leftmax = Math.max = 3

leftmax = 3 ; height[8] = 2 ;leftmax = Math.max = 3

leftmax = 3 ; height[9] = 1 ;leftmax = Math.max = 3

leftmax = 3 ; height[10] = 2 ;leftmax = Math.max = 3

leftmax = 3 ; height[11] = 1 ;leftmax = Math.max = 3

例如当i是下标4的时候我左边最高值是2.

int rightmax = 0; for(int j = i + 1 ; j < height.length ; j++){ rightmax = Math.max(rightmax , height[j]); } 

右边也是同理,不过起点变成i+1,因为要找的是i右边最高的柱子,终点自然是数组结束。

int water = Math.min(leftmax , rightmax) - height[i]; total += Math.max(water , 0);

Math.min就比较两个数字谁更小

int一个water用来记录当前下标接水的数量,然后按照我们最开始讲的解题关键:看左右柱子谁低,拿低的减去盆底,这里的盆底就是height[i]

total上面解释了用来存储总接水量。为什么要加一个判断是否大于0,因为可能存在water为负数的情况(按照现实根本不存在数量为负数)

例如这种情况,当height[i]为1,左边最高柱子为2,右边最高柱子为3。

(2<3) 2 - 5 = -3 这个时候我们的结果就出现负数所以要加以判断。

等所有代码编写完毕,返回total即可。

这样,这道题思考成本最低的暴力解法你就会了,太棒了。但是当我们提交会发现

那有人说了:人家学就是要能提交成功,你教个不能提交的不是捣乱吗别急,下面我讲的是基于暴力解法的优化流解法。

(2)优化流解法

class Solution { public int trap(int[] height) { if(height == null || height.length<= 2)return 0; int n = height.length; int[] leftmax = new int[n]; int[] rightmax = new int[n]; leftmax[0] = height[0]; for (int i = 1 ; i < n ; i++){ leftmax[i] = Math.max(leftmax[i - 1] , height[i]); } rightmax[n - 1] = height[n - 1]; for(int i = n - 2 ; i >= 0 ; i--){ rightmax[i] = Math.max(rightmax[i + 1] , height[i]); } int total = 0; for(int i = 0 ; i < n ; i++){ total += Math.min(leftmax[i],rightmax[i]) - height[i]; } return total; } }

为了避免重复计算左右最大值,先提前用两个数组存好每个元素位置的左、右最大值

为什么暴力解法会出现重复计算?比如

计算 i=5 的左最大值:需要循环 0~4,比较 5 次;

计算 i=6 的左最大值:又要循环 0~5,比较 6 次;

(其中 0~4 的比较是完全重复);

其实这部分代码从根本性和步骤思维逻辑和暴力解法完全一模一样。唯一改变的是用两个数组存储每个元素对应的最大左右柱子值,当我遍历到某个元素时,直接从数组里用就好了。省去了大部分重复计算的时间。那如果说你看到这出现了一个问题

这不就是哈希表吗,那么恭喜你,已经超过了99%的新手这是一种抽象类比和算法思维。给自己鼓鼓掌,太厉害了。能想到的新手朋友们在评论区留个言,我会随机抽几个人送福利

这就是哈希表的表现形式,通过我的下标取相对应的左右最大值。

所以这个方法的思维逻辑可以看暴力解法,那个会了这个百分比会了,而且只会觉得:我去不早说。这个时候你去提交答案就会出现

你学会这个方法就代表你已和绝大多数人的思维逻辑一样了,那有聪明的人又要问了:我就是好学还有没有其他更好的方法。好!下面,就是接雨水最完美的方法-------------双指针!

(3)双指针解法

class Solution { public int trap(int[] height) { if(height == null || height.length<= 2)return 0; int n = height.length; int left = 0; int right = n - 1; int leftMax = 0; int rightMax = 0; int totalWater = 0; while (left < right) { leftMax = Math.max(leftMax, height[left]); rightMax = Math.max(rightMax, height[right]); if (leftMax < rightMax) { totalWater += leftMax - height[left]; left++; } else { totalWater += rightMax - height[right]; right--; } } return totalWater; } }

这居然没用到for循环,有点意思。其实双指针的本质就不存在for循环放指针,而是定义空变量,用while设置条件驱动变量作为指针去++和--。

我先解释一下:

left是左指针

right是右指针(起点是数组最右端所以是n - 1)

leftmax是左边柱子的最大值

rightmax是右边柱子的最大值

totalWater是总接水量

这个方法的思维逻辑是左指针在最左边,右指针在最右边,只要左指针在右指针左边就一直动,直到两个指针重合。动的过程中要实时更新左指针和右指针的最大值。为了避免出现负值,当左边柱子最大值小于右边柱子最大值时,移动左指针;反之移动右指针。在移动的过程中,直接做最大值减去盆底的操作得到当前元素的接水量。

下面,我将全程演示一遍过程:

left=0 ,right=11 ,leftmax = 0 ,rightmax = 0-->1 ,该元素接水量= 0(leftmax)-0(height[left])=0 ,totalWater = 0 ,(leftmax<rightmax左指针移动)

left=1 ,right=11 ,leftmax = 0-->1 ,rightmax = 1 ,该元素接水量= 1(leftmax)-1(height[left])=0 ,totalWater = 0 ,(leftmax=rightmax右指针移动)

left=1 ,right=10 ,leftmax = 1 ,rightmax = 1-->2 ,该元素接水量=2(rightmax)-2(height[right)=0 ,totalWater = 0 ,(leftmax<rightmax左指针移动)

left=2 ,right=10 ,leftmax = 1 ,rightmax = 1-->2 ,该元素接水量= 1(leftmax)-0(height[left])=1,totalWater = 0-->1 ,(leftmax<rightmax左指针移动)

left=3 ,right=10 ,leftmax = 1-->2 ,rightmax = 2,该元素接水量= 2(leftmax)-2(height[left])=0,totalWater = 1 ,(leftmax=rightmax右指针移动)

left=3 ,right=9 ,leftmax = 2 ,rightmax = 2 ,该元素接水量= 2(rightmax)-1(height[right])=1,totalWater = 1-->2 ,(leftmax=rightmax右指针移动)

left=3 ,right=8 ,leftmax = 2 ,rightmax = 2 ,该元素接水量= 2(rightmax)-2(height[right])=0,totalWater = 2 ,(leftmax=rightmax右指针移动)

left=3 ,right=7 ,leftmax = 2 ,rightmax = 2-->3,该元素接水量=3(rightmax)-3(height[right])=0,totalWater = 2 ,(leftmax<rightmax左指针移动)

left=4 ,right=7 ,leftmax = 2 ,rightmax = 3,该元素接水量=2(leftmax)-1(height[left])=1,totalWater = 2-->3 ,(leftmax<rightmax左指针移动)

left=5 ,right=7 ,leftmax = 2 ,rightmax = 3,该元素接水量=2(leftmax)-0(height[left])=2,totalWater = 3-->5 ,(leftmax<rightmax左指针移动)

left=6 ,right=7 ,leftmax = 2 ,rightmax = 3,该元素接水量=2(leftmax)-1(height[left])=1,totalWater = 5-->6 ,(left=right循环结束)

结果:totalWate = 6

那么这道题的思维逻辑就结束了,如果到这你还能理解,恭喜你,我为你再讲一个方法

(简化版双指针解法)

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

        这里的巧思就是对第一位元素的处理,标准版双指针是为了循环判断,把第一为leftmax赋值成0,多判断了一个无用的第一位元素。而简化版是直接给leftmax赋值成height[0]省去对第一位元素的处理。并且标准版是先更新max值,简化版是后更新max值(因为去掉了第一位元素处理,在这个空缺提前对下一位元素进行处理)但是其实本质没有任何区别。

举个形象的例子:标准版是先洗碗再吃饭再起身,简化版是先吃饭再起身再洗碗。没有啥区别

提交这个方法发现

哇,给自己鼓鼓掌!!!

3.总结

        你们有没有发现,单看代码看死了都理解不了里面的逻辑顺序,但是画个图自己走一遍过程就直接茅塞顿开了。所以做任何理科性质的东西画画图,想到画图,多画图,理解性画图是破题的关键。

本篇是用java来演示的,但是整体思路逻辑已经讲的很细了,理解透用什么写都能写出来。如果有什么不理解的或者有什么对我的建议都可以评论留言。

Read more

【前端实战】构建 Vue 全局错误处理体系,实现业务与错误的清晰解耦

【前端实战】构建 Vue 全局错误处理体系,实现业务与错误的清晰解耦

目录 【前端实战】构建 Vue 全局错误处理体系,实现业务与错误的清晰解耦 一、为什么要做全局错误处理? 1、将业务逻辑与错误处理解耦 2、为监控和埋点提供统一入口 二、Vue 中的基础全局错误处理方式 1、Vue 中全局错误处理写法 2、它会捕获哪些错误? 3、它不会捕获哪些错误? 4、errorHandler 的参数含义 三、全局错误处理的进阶设计 1、定义“可识别的业务错误” 2、在 errorHandler 中做真正的“分类处理” 3、补齐 Promise reject 的捕获能力 4、错误处理的策略化封装 四、结语         作者:watermelo37         ZEEKLOG优质创作者、华为云云享专家、阿里云专家博主、腾讯云“

By Ne0inhk
Microsoft Edge WebView2 Runtime(运行库)快速部署 + 调试指南(精简实用、适配开发 + 用户双场景)

Microsoft Edge WebView2 Runtime(运行库)快速部署 + 调试指南(精简实用、适配开发 + 用户双场景)

WebView2运行库 v143.0.3650.139 x64 精简安装(下载) 一、WebView2 Runtime 快速安装部署(用户 / 开发通用,必做) ✅ 1. 系统预装情况 ▸ Windows 11 系统 默认自带 常青版 WebView2 运行库,无需手动安装;▸ Windows 10/7/8.1 需手动安装,缺失则调用 WebView2 控件的软件会弹窗报错「缺少 WebView2 运行环境」。 ✅ 2. 两种官方安装方式(推荐) 方式 1:常青版(Evergreen Runtime)- 首选 ▸ 特点:体积小(引导包仅

By Ne0inhk
Qt与Web混合编程:CEF与QCefView深度解析

Qt与Web混合编程:CEF与QCefView深度解析

Qt与Web混合编程:CEF与QCefView深度解析 * 1. 引言:现代GUI开发的融合趋势 * 2. Qt与Web集成方案对比 * 3. CEF核心架构解析 * 4. QCefView:Qt与CEF的桥梁 * 5. 实战案例:智能家居控制面板 * 6. 性能优化策略 * 7. 调试技巧大全 * 8. 安全加固方案 * 9. 未来展望:WebComponent集成 * 10. 结语 1. 引言:现代GUI开发的融合趋势 在当今的桌面应用开发领域,本地GUI框架与Web技术的融合已成为不可逆转的趋势。Qt作为成熟的跨平台C++框架,与Web技术的结合为开发者提供了前所未有的灵活性: * 本地性能 + Web动态性 = 最佳用户体验 * 快速迭代的Web前端 + 稳定可靠的本地后端 * 跨平台一致性 + 现代UI效果 35%25%20%20%混合应用优势分布开发效率UI表现力跨平台性性能平衡 2. Qt与Web集成方案对比 方案优点缺点适用场景Qt WebEngine官方支持,

By Ne0inhk
前端真的能防录屏?EME(加密媒体扩展) DRM 反录屏原理 + 实战代码

前端真的能防录屏?EME(加密媒体扩展) DRM 反录屏原理 + 实战代码

🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志 🎐 个人CSND主页——Micro麦可乐的博客 🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战 🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战 🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解 🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用 🍎 《前端技术》专栏以实战为主介绍日常开发中前端应用的一些功能以及技巧,均附有完整的代码示例 ✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧 💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程 🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整 👍《Spring Security》专栏中我们将逐步深入Spring Security的各个

By Ne0inhk