最详细最通俗易懂的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来演示的,但是整体思路逻辑已经讲的很细了,理解透用什么写都能写出来。如果有什么不理解的或者有什么对我的建议都可以评论留言。