Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it is able to trap after raining.
Example:
Input: [0,1,0,2,1,0,1,3,2,1,2,1]
Output: 6
解法一
先说说自己超时的算法思路,也顺便把一种暴力解法展示出来,然后介绍别人家的高级算法
,也算是抛砖引玉。
很明显,刚看到这题,我就想着以行为思路来考虑,什么意思呢?举个例子,如输入为[0,7,1,4,6]
,result
为我们要返回的结果。首先我们需要知道有0的地方代表着一个可以存水的坑,然后进行如下操作:
- 先将两边的0去掉 => [7,1,4,6]
- result += 0的个数 * 非零元素中的最小值
- 原数组中每个数减去上一步中的最小值(小于0则保持为0)
- 返回第一步,直到数组个数小于3则退出
于是就有:
[7,1,4,6] => result += 0 => [6,0,3,5] => result += 1x3 => [3,0,0,2] => result += 2x2 => [1,0,0,0] => [1] => exit.
总的而言还是比较麻烦。代码逻辑没问题,就是时间复杂度较高,约为O(n^2)级别, 取决于其中数的分布。
与之相对应的就是一种列的思维方式。
解法一
这里的解法与后面的相关解法出发思路都是相同的,即根据两端方块的高度来决定每一个方块的可储水量,然后进行累加,储水量求的是面积,而我们每次仅考虑一个方块,宽度为1,则数值上其就应该等于储水高度。
暴力法的思路就是遍历每一个方块,分别找到包含它本身向左、向右的最大高度left_max, right_max
,那么很显然储水高度由left_max, right_max
中的较小者以及自身的高度baseHeight
来决定:
min(left_max, right_max) - baseHeight
def trap(height):
n = len(height)
left_max, right_max = 0, 0
# 返回结果
result = 0
# 考虑每一个方块
for i in range(n):
# 它所有左边的方块中找到left_max
for j in range(i, -1, -1):
left_max = max(left_max, height[j])
# 它所有右边的方块中找到right_max
for j in range(i, n, 1):
right_max = max(right_max, height[j])
result += min(left_max, right_max) - height[i]
return result
很明显,时间复杂度依然较高,为O(n^2)。考虑一种用空间换时间的方法。上一种暴力法我们每选择一个方块,就重新进行O(n)的遍历找到它的left_max, right_max
,其实是可以通过先循环遍历记录下来,之后直接索引取值即可,也可以算的上是动态规划的一种思想。
def trap(height):
n = len(height)
left_max, right_max = 0, 0
# 记录每个索引处的最大左、右高度
left = [0] * n
right = [0] * n
# 返回结果
result = 0
# 从左边遍历 找到left_max
for i in range(n):
left_max = max(left_max, height[i])
left[i] = left_max
# 从右边遍历 找到right_max
for j in range(n-1, -1, -1):
right_max = max(right_max, height[j])
right[j] = right_max
# 然后就是熟悉的操作
for k in range(n):
result += min(left[k], right[k]) - height[k]
return result
这就是所谓的动态编程
。时间复杂度上优化到了O(n)级别,相应的开辟了两个数组保存最大高度,空间复杂度也为O(n)。
解法二
好了,在解法一的启发下,就可以献出大招,双指针法了。双指针法能更灵活地在移动指针的过程中确定界
,也就是我们的left_max, right_max
,并且使用了常数级别的空间复杂度。废话不多说,看代码:
def trap(height):
n = len(height)
# 双指针
i, j = 0, n-1
result = 0
# 如果left_max处的高度小于right_max处的高度
# 我们有理由改变左指针i来累加储水量
left_max, right_max = 0, 0
while i < j:
left_max = max(left_max, height[i])
right_max = max(right_max, height[j])
if left_max < right_max:
result += left_max - height[i]
i += 1
else:
result += right_max - height[j]
j -= 1
return result
其实仔细一看,思路和暴力法没差。但是相对而言,算法进步了不少。
解法三
这是一种借助栈
的思路,来找到上面所提到界
的方法。其出发点就是,将所有遍历的索引入栈,当遍历到的方块高度大于栈顶的高度,这就意味着此刻栈顶的高度不仅小于当前遍历值,也小于它自身栈的前一个高度。也就是说我们就找到了界和baseHeight
。看代码吧:
def trap(height):
n = len(height)
stack = []
result = 0
for i in range(n):
# 栈非空时,不断比较弹出并累加result
while stack and height[stack[-1]] < height[i]:
# 弹出
pop_ = stack[-1]
stack.pop()
#如果栈为空
if not stack:
break
# 宽度
w = i - stack[-1] - 1
# 高度差
# 界的最小值-基准高度
h = min(height[i], height[stack[-1]]) - height[pop_]
result += h
# 入栈
stack.append(i)
return result