提出问题
接雨水是一道经典的算法题,在LeetCode中标注是hard,有一定难度。
题目如下:
分析问题
这个问题咋一看几乎无从下手。当我们将思维集中在每一个柱子的时候,就能够有所发现。每个柱子能承接的雨水量取决于他左边和右边最高柱子(两者取小,因为水会从小的那边流走)。
经过以上分析,算法很容易设计。遍历每个柱子,然后取这这个柱子的左右两侧最高点的小值,累加就能得到所有柱子能接多少雨水。
实现代码
class Solution {
public int trap(int[] height) {
int total=0;
// 第一个和最后一个柱子不可能有雨水,所以这里直接跳过。
for( int i=1; i<height.length-1; i++ ){
int minHeight = Math.min(findLeftMax(height,i-1),findRightMax(height,i+1));
if(height[i]<minHeight){
total+=minHeight-height[i];
}
}
return total;
}
public int findLeftMax(int[] height, int end ){
int leftHeight = height[end];
for( int i=end-1; i>=0; i-- ){
leftHeight = Math.max(leftHeight,height[i]);
}
return leftHeight;
}
public int findRightMax(int[] height, int start ){
int rightHeight = height[start];
for( int i=start; i<height.length; i++ ){
rightHeight = Math.max(rightHeight,height[i]);
}
return rightHeight;
}
}
改进
上面代码已经解决了问题,但是时间复杂度为
O
(
n
2
)
O(n^2)\,
O(n2)
那么有没有办法改进呢。
我们看到,整个遍历是从左到右遍历的,那么当前点左边的最大值肯定是我们遍历过的点的值,只要每次都拿最大的值就不用再去查询。所以可以去掉一个for循环。如图所示
实现代码
class Solution {
public int trap(int[] height) {
int leftHeight = height[0];
int total=0;
for( int i=1; i<height.length-1; i++ ){
int minHeight = Math.min(findMax(height,i+1),leftHeight);
if(height[i]<minHeight){
total+=minHeight-height[i];
}
leftHeight = Math.max(leftHeight,height[i]);
}
return total;
}
public int findMax(int[] height, int start ){
int rightHeight = height[start];
for( int i=start; i<height.length; i++ ){
rightHeight = Math.max(rightHeight,height[i]);
}
return rightHeight;
}
}
分析
从上面的代码可以看到,这里去掉了一个循环,但是复杂度的量级还是没有变化,依旧是两层for循环。那么还能优化吗,因为我们是从左到右遍历所以可以将左边寻找最大值的循环去掉。但是右边怎么办呢。可以用空间换时间,将右边的最大值提前算好保存起来。 可以将每个位置对应的右边最大值保存在一个数据中。如图:
代码实现——局部动态规划
class Solution {
public int trap(int[] height) {
int[] rightHeight = new int[height.length];
rightHeight[height.length-1] = height[height.length-1];
// 利用动态规划先将最大值保存在数组中
for( int i=height.length-2; i>=0; i-- ){
rightHeight[i] = Math.max(rightHeight[i+1],height[i]);
}
int leftHeight = height[0];
int total=0;
for( int i=1; i<height.length-1; i++ ){
int minHeight = Math.min(rightHeight[i+1],leftHeight);
if(height[i]<minHeight){
total+=minHeight-height[i];
}
leftHeight = Math.max(leftHeight,height[i]);
}
return total;
}
}
至此我们实现了时间复杂度为O(n)的解法,使用了额外的数组空间。还能优化吗?
继续分析
从上面的分析我们可以得出,需要找到当前柱子左右的最大值,但是一定要是最大值吗?我们知道水会从低的一端流走,那么只要保证一端是最大值且另外一端比当前端的值大(不一定是最大值)就可以。因为当前端是低的一头,水都是从这里流走的,另外一边是不是最大值已经不重要了。(举个例子,有两个人遇到了老虎,一个人系紧鞋带就要爬,另外一个说你能跑过老虎吗?这个人说,只要跑过你就行了)其实就是说你管我是不是最大的,只要水不是从我这边流走就好了。
如图分析:
如上分析,我们只要每次计算当前左右极值的小值一边柱子就可以计算出整个存水量。
代码实现——双指针
class Solution {
public int trap(int[] height) {
int total = 0;
int left = 1;
int right = height.length-2;
int leftMax = height[0];
int rightMax = height[height.length-1];
//退出条件,确保除了两头以外所有柱子都计算到。
while(left<=right){
if( leftMax<=rightMax ){
// 那边小计算那边,原理看上面图解。
total+=(leftMax > height[left]? leftMax-height[left]:0 );
leftMax = Math.max(leftMax,height[left]);
left++;
} else {
total+=(rightMax > height[right]? rightMax-height[right]:0 );
rightMax = Math.max(rightMax,height[right]);
right--;
}
}
}
至此,我们使用了O(0)的空间复杂度和O(n)的时间复杂度解决了这个问题。