当青训营遇上码上掘金
主题 4:攒青豆
现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
以下为上图例子的解析:
输入:height = [5,0,2,1,4,0,1,0,3]
输出:17
解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17 个单位的青豆。
暴力求解
当我们拿到一道题,没有很好的思路时,我们首先总是想到能不能用暴力的手段解决问题,当然这道题也是可以的.
那么这道题如何暴力求解呢,我们需要遍历数组的每一个位置。对于每一个位置,我们需要枚举出来它左边的最大高度和它右边的最大高度,然后取两边高度小的那个在减去当前柱子的高度,这就是这个位置能装的青豆的数量,在遍历的过程中将每一个位置能存放的青豆数量累积起来,就可以得到我们要的结果.
总结来说就是每个位置的柱子小于两边的高度时,能装的青豆数为:
res += min(leftMax,rightMax)-height[i]
代码实现:
func trap(height []int) int {
// 小于三个柱子,肯定不会有水,直接返回
if len(height) <= 2 {
return 0
}
res := 0
// 第一根柱子和最后一根柱子一定不会有青豆,所以从第二根柱子开始遍历
for i := 1; i < len(height)-1; i++ {
// 求出左边的最大值
leftMax := 0
for j := 0; j < i; j++ {
leftMax = max(leftMax, height[j])
}
// 求出右边的最大值
rightMax := 0
for j := i + 1; j < len(height); j++ {
rightMax = max(rightMax, height[j])
}
// 求出当前柱子能存的青豆
sideHeight := min(leftMax, rightMax)
// 当前柱子的高度小于两边的最大值,才能装青豆
if height[i] < sideHeight {
res += sideHeight - height[i]
}
}
return res
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
暴力求解的时间复杂度为O(n^2),空间复杂度为O(1)。这种方法显然我们不能接受。
暴力求解优化:
从上面代码我们可以看到,在遍历每一个位置时我们去求它的左右最大值,导致在循环里重复很多次,优化是我们可以先求出每个位置的左右最大值,存在数组里,这样就不用循环遍历时去求取,也就是对数据进行预处理。
代码实现:
func trap(height []int) int {
// 小于三个柱子,肯定不会有水,直接返回
if len(height) <= 2 {
return 0
}
res := 0
// 用两个数组计算每个柱子左边的最大值和右边的最大值
// 预处理要带上第一个和最后一个柱子,因为第一个和最后一个柱子有可能是最大值
leftMax := make([]int, len(height))
leftMax[0] = height[0]
for i := 1; i < len(height); i++ {
leftMax[i] = max(leftMax[i-1], height[i])
}
rightMax := make([]int, len(height))
rightMax[len(height)-1] = height[len(height)-1]
for i := len(height) - 2; i >= 0; i-- {
rightMax[i] = max(rightMax[i+1], height[i])
}
// 第一根柱子和最后一根柱子一定不会有青豆,所以从第二根柱子开始遍历
for i := 1; i < len(height)-1; i++ {
sideHeight := min(leftMax[i], rightMax[i])
// 当前柱子的高度小于两边的最大值,才能装青豆
if sideHeight > height[i] {
res += sideHeight - height[i]
}
}
return res
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
优化后虽然空间复杂度变高了O(n),但时间复杂度低了O(n)
双指针
上面解法空间复杂度是O(n),那么我们能不能让空间复杂度降低呢?是可以的。上面解法它对于每个位置都找到了两边的最大值, 但实际上, 青豆数量是由两者中的较小值决定的, 所以只要找到较少的那个即可, 没必要找到较大的那个的最大值。我们只需要两个变量去记录leftMax和rightMax,在遍历时不断更新leftMax和rightMax,这样不用数组空间复杂度就降下来了。 具体解法就是left和right双指针从头和尾向中遍历,在遍历时更新leftMax和rightMax,当height[left] < height[right]时,right刚刚找到自己目前的最大值, left在寻找更大值的过程中。 所以此时必然有leftMax < rightMax, 这样就满足了我们寻找两者中较小者的要求. 所以就不用比较leftMax和rightMax。结果就是res += leftMax - height[left] 代码实现:
func trap(height []int) int {
if len(height) <= 2 {
return 0
}
res := 0
// 左右两边的最大值
leftMax, rightMax := 0, 0
// left,right要从头和尾开始,因为第一根柱子或者最后一根柱子有可能是最大值
left, right := 0, len(height)-1
for left < right {
// 更新最大值
leftMax = max(leftMax, height[left])
rightMax = max(rightMax, height[right])
if height[left] < height[right] {
// 此时一定满足leftMax < rightMax
res += leftMax - height[left]
left++
} else {
res += rightMax - height[right]
right--
}
}
return res
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
时间复杂度O(n),空间复杂度O(1),应该是最优解了,至于其他题解不阐述了。