题目
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
输入:height = [4,2,0,3,2,5]
输出:9
思路
暴力解法
暴力解法虽然简单,但是这道题的暴力思路一时我还没反应过来,花了挺久时间才想明白咋回事。
首先,大体思路就是,遍历每个柱子,也就是遍历数组每个元素,然后每个元素为中心,向左、右两侧遍历寻找比它高的最高的柱子,这样就能计算出这个柱子上方能接多少水。
看不懂没关系,开始我也看不懂,接下来结合代码详细说一下就懂了:
1.
首先,最外层for循环:for i in range(1, size-1)
遍历柱子,每个柱子作为桶底,向左右两侧寻找高柱子作为桶的左右两个边
由于数组的开头、结尾这两个柱子是边界,它们当桶底不可能形成一个桶,所以遍历的时候,是从第2根柱子开始到倒数第2根柱子结束,所以是range(1,size-1),而不是range(size)
2.
接下来,每次遍历拿到作为桶底的柱子,以它为中心,向左右两侧再遍历,寻找比它高的最高的柱子,作为桶底的左右两个边
for i in range(1, size-1):
max_left,max_right = 0,0
# 寻找i左边最高的柱子
for j in range(i+1):
max_left = max(height[j],max_left)
# 寻找i右边最高的柱子
for k in range(i,size):
max_right = max(height[k], max_right)
ans += min(max_left,max_right) - height[i]
这里有几个点,比较不容易想明白:
为什么是向左右两边寻找柱子的时候,要从它自己开始一直找到开头、结尾,而不是只看它左右的那两个柱子;而且为什么要找最高的,而不是只要比它高就行:
因为这里容易陷入一个思考误区,就是觉得一个柱子能够接水,只要它左右相邻的两个柱子比它高,把它围起来,围成一个桶,就可以接水了。
其实并非如此,一个柱子能接水,它左右两边是要有比它高的柱子,但是这两个柱子并不是一定要和它相邻;并且,这两个柱子不仅比它高,而且还是最高的,看图就明白了:
比如上图中,柱子a是当前作为桶底的柱子,a的左右两侧柱子-a1、a1确实比它高,而且还和它相临,但是再向左右两侧看,-a2、a2更高,他们围城了一个更大的桶。所以,要向左右两侧一直遍历到开头、结尾,并且要找到最高的柱子作为桶的两边。我们在计算水的时候,不要想着怎么去直接计算-a2到a2围城的这个桶里面的水有多少,而是指考虑某个柱子上方的水是多少就可以了,想象成柱状图,每个柱子上面的水也是一柱一柱的,不要被“水”这个字眼迷惑了。
比如,只计算a柱子上方的水,这个桶较低的一边是-a2,高度为2,a的宽度是1,所以a柱子上方的水就是2。
同理,也可以计算出-a1柱子上方的水,这时要注意,-a1柱子本身高度为1,占据了空间,所以要把它自身的高度减掉。
同理,也可以计算出a1柱子上方的水
最后,把-a1、a、a1三个柱子上方的水,加起来,就是-a2、a2围城的桶里的水了。
同理,遍历每个可以作为桶底的柱子,计算出每个桶底上方的水,最后累加起来,就是最终的答案。- 遍历桶底柱子的时候,我们不需要开头、结尾的两个柱子,因为他俩不可能作为桶底。
而在寻找左右两测最高的桶边柱子时,要包括开头、结尾的两个柱子,因为它们可以作为桶边。 - 寻找左右桶边的时候,把当前桶底这个柱子,也算在遍历范围中了,因为如果左右两边没有比它高的柱子,那么它就既是桶底、也是桶边,可以想象成它就是一块木头桩子,显然它上面是不能接水的。所以,最后在计算接水的时候,用桶边较低高度-桶底自身高度,对它来说就是自己减自己等于0,刚好符合逻辑。
当然,你也可以在遍历时,不把桶底这个柱子算在遍历范围内,只不过遇到左右没有比它高的柱子这种情况时,还要单独写代码处理,就比较麻烦,所以这样写算是一个小技巧。
def trap(height) -> int:
size = len(height)
ans = 0
# 遍历每个可以作为桶底的柱子,开头、结尾两个柱子不能作为桶底,不在遍历范围内
for i in range(1,size-1):
max_left,max_right = 0,0
# 寻找i柱子左侧比自己高的最高柱子,没有的话i自己就是最高柱子
# 开头的柱子可以作为桶边,在遍历范围内
for j in range(i+1):
max_left = max(height[j],max_left)
# 寻找i柱子右侧比自己高的最高柱子,没有的话i自己就是最高柱子
# 结尾的柱子可以作为桶边,在遍历范围内
for k in range(i,size):
max_right = max(height[k], max_right)
# 较低的桶边柱子高度 - 桶底柱子高度,就是桶底柱子上方的储水量
# 每个桶底柱子上方储水量累加,就是最终答案
ans += min(max_left,max_right) - height[i]
return ans
时间复杂度:O(n^2),每遍历一个元素,就要遍历一次数组
空间复杂度:O(1)
动态规划
理解了暴力解法的思路,动态规划就很容易了。
动态规划的原理和暴力解法一模一样,只不过先提前遍历数组,从左向右遍历一遍,找出每个柱子左侧的最高柱子的高度;从右向左遍历一遍,找出每个柱子右侧的最高柱子的高度。
这样就事先找出了每个柱子作为桶底,它左右两侧的桶边的最大高度。然后直接计算就可以得出每个柱子作为桶底,它上方能接多少雨水了。
和暴力解法一样,找最高桶边柱子的时候,开头、结尾两个柱子是在查找范围内的。
而计算雨水的时候,开头、结尾两个柱子不可能接的住水,所以不在计算范围内。
def trap(height) -> int:
n = len(height)
# 存储最大高度的数组
max_left = [0]*n
max_right = [0]*n
# 从左往右遍历,寻找每个桶底柱子左侧的最高柱子
for i in range(n):
# i=0左侧开头柱子就是它自己
# 其他柱子自己和之前的比较,高的就是最高柱子
max_left[i] = max(height[i],height[i] if i==0 else max_left[i-1])
# 从右往左遍历,寻找每个桶底柱子右侧的最高柱子
for i in range(n-1,-1,-1):
# i=n-1右侧结尾柱子就是它自己
# 其他柱子自己和之前的比较,高的就是最高柱子
max_right[i] = max(height[i],height[i] if i==n-1 else max_right[i+1])
# 计算每个桶底柱子,上方能接多少水,此时计算范围是range(1,n-1)不含开头、结尾的柱子
ans = sum(min(max_left[i],max_right[i])-height[i] for i in range(1,n-1))
return ans
时间复杂度:O(n),只遍历了3次数组
空间复杂度:O(n),存储最大高度的数组使用了额外的空间
单调栈
遍历每个柱子,如果当前柱子高于栈顶的柱子,那说明栈顶的柱子有可能作为桶底,形成桶可以接水。将栈顶的桶底柱子弹出,此时栈顶的柱子是桶的左边,计算当前这个桶的储水量。
如果当前柱子,还高于此时栈顶的柱子,说明这个柱子之前是桶的左边,现在也可能作为桶底去接水,将这个栈顶柱子也弹出,此时栈顶的柱子是新的桶的左边,计算新的桶的储水量。
重复上述循环,直到当前柱子低于栈顶柱子,此时栈顶柱子不可能是桶底了,不能再形成桶了,或者栈空了,则继续下一轮遍历。
总结:
当前所遍历的柱子,看作桶的右边
栈中栈顶的柱子,看作桶底
栈中栈顶前一个柱子,看作桶的左边
就是通过循环、入栈操作,看看每个柱子作为桶的右边,它和前面的柱子能否形成一个桶
形成桶就计算这个桶底的储水量,计算完成后,将这个桶底柱子弹出栈
再往前看,能否和前面的柱子形成一个桶
以此类推
def trap(height) -> int:
ans = 0
stack = []
for i in range(len(height)):
# 栈里有柱子,右柱 高于 桶底
while stack and height[i] > height[stack[-1]]:
# 此时栈顶 是桶底柱子的下标
bottom = stack.pop()
# 弹出桶底空栈了,说明这个桶没有左柱,跳出
if not stack:
break
# 否则,就是一个桶,计算这个桶底的储水量
# 桶底被弹出后,此时栈顶是左柱的下标
# 桶的宽度=右柱下标 - 左柱下标 -1
w = i - stack[-1] -1
# 桶能储水的高度 = 较低的桶边 - 桶底的高度
# 假如遇到 左柱、桶底 一样高的情况,则经过计算这个桶能储水的高度为0,无法储水
# 会继续while循环,继续向前寻找桶底、左柱
h = min(height[stack[-1]], height[i]) - height[bottom]
ans += w*h
# 当前右柱,计算完它作为右柱,和前面的柱子可能形成的桶的储水量后,它也入栈,作为后面柱子的左柱、桶底
stack.append(i)
return ans
时间复杂度:单次遍历O(n),每个条形块最多访问两次(由于栈的弹入和弹出),并且弹入和弹出栈都是O(1)的。
空间复杂度:O(n)。 栈最多在阶梯型或平坦型条形块结构中占用O(n)的空间
双指针
平时常见的for循环,可以看作是单指针的,从开始遍历到结束
双指针,就可以想象成有两个指针,一个从开头往后遍历,一个从结尾向前遍历,直到两个指针相遇,整个数组也就遍历完了。
我们定义两个指针left、right,left从开头向右移动遍历,right从结尾向左移动遍历
指针指向的元素,作为桶底。但是现在有left、right两个指针,选哪个作为桶底呢?
选择高度较低的那个作为桶底,因为一个桶要想能够接水,桶边不可能比桶底还低。
比如,当前left指向高度为1的柱子,right指向了高度为3的柱子,那么left指针指向的柱子就是桶底。
接下来就计算,left指针指向的柱子上方能接多少水。一个桶能接多少水,是由较短的桶边决定的,加入left指针左侧的柱子中,最大高度为2,那么就是由这个高度为2的柱子决定了当前left指向的桶底能存2-1=1的水。
这里可能会问,那如果left左侧柱子中,存在最大高度是3,30,或者300的柱子呢?还是要用左侧柱子的最大高度 - 桶底的高度吗,这样不就错了吗?
其实,我们的逻辑是,在选择left、right指向的柱子哪个作为桶底时,永远选择较矮的那个作为桶底。
因为如果上述情况发生,假设左侧有一个高度是30的柱子,那么left指针就会一直停在30这个柱子这里,因为right指针遍历的柱子中没有比30高的,桶底都在right指向的这边,除非随着遍历的进行,right遇到了一个高于30的柱子,桶底才到了left这边,left才会开始移动。
所以不可能会出现,选择了较矮的left做桶底,结果left左边的最大高度会高于right的高度的情况。
right的计算也和left一样。
def trap(height) -> int:
ans = 0
left,right = 0, len(height)-1
left_max,right_max = 0,0
# 两个指针同时遍历数组,left从左向右遍历,right从右向左遍历
# 当两个指针相遇时,即left=right时,数组就遍历完了
while left < right:
# 用来保存指针扫描过的柱子的最大高度
left_max = max(left_max, height[left])
right_max = max(right_max, height[right])
if height[left] < height[right]:
ans += left_max - height[left]
left +=1
else:
ans += right_max -height[right]
right -=1
return ans
时间复杂度:O(n)
空间复杂度:O(1)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。