经典算法--最大存水量问题

题目:给出一个数组代表围柱的高度,求能围柱的最大的水量,例如数组{ 5,2,3,2,4 },最大水量为5。

如下图:黄色部分为围柱,绿色部分是能够围住的水,图中围柱的高度依次为 5,2,3,2,4最多能围住的水量是5。

这里写图片描述

思路:求出每个柱子上面能够存多少水,然后将每根柱子的存水量相加便能得到总的存水量,为求出每根柱子上能够存多少水,就要求出每根柱子左边最高的和右边最高柱子,然后用两者的最小值减去当前柱子的高度。 例如图中从左到右第二根柱子的高度为2,它左边最高柱子的值为5,右边最高柱子的值为4,因此它的最大存水量为 Min(4,5)-2=2。

解法1

利用上面思路,从左到右遍历每根柱子,遍历的时候求出每根柱子左边最高和右边最高柱子的值,然后利用s两者的最小值减去当前柱子的高度就行了。时间复杂度O(n^2),空间复杂度O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static int water1(int []arr)
{
int water=0;
//water 用于保存最大存水量
if(arr==null||arr.length<=1)
return 0;
//如果只有一根柱子围不住水。
int leftLargest=0,rightLargest=0;
//leftLargest,rightLargest 分别用于保存遍过程中,当前元素左边最大值,和右边最大值。
for(int i=0;i<arr.length;i++)
{
leftLargest=0;
rightLargest=0;
for(int j=0;j<i;j++)
leftLargest=Math.max(leftLargest,arr[j]);
//先求出当前元素左边最大值。
for(int j=arr.length-1;j>i;j--)
rightLargest=Math.max(rightLargest, arr[j]);
//最求出当前元素右边最大值
water+=Math.min(leftLargest, rightLargest)>arr[i]?Math.min(leftLargest, rightLargest)-arr[i]:0;
//左边最大值和右边最大值的最小值与当前元素比较如果小于当前元素,则当前元素上水量为0,围不住水,如果大于当前元素,则减去当前元素得到存水量。
}
return water;
}

注意:
① 如果当前柱子大于它左右最大值的任何一个是存不了水的。

解法2

分析上面的算法发现算法的时间复杂度为O(n^2)的原因是对于每个元素都要从左到右,和从右到最左遍历其两边最大值,假如使用两个数组 left[ ] , right[ ]来保存每个元素左边最大值,右边最大值的话,这样就不用每次都遍历了,因此时间复杂度可以减少到O(n),但空间复杂度为O(n),典型的空间换时间算法。如下图:对于数组{ 5, 2 , 6 , 2 , 4 }它的左右数组如下
左数组:
这里写图片描述
右数组
这里写图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static int water1(int []arr)
{
int water=0;
if(arr==null||arr.length<=1)
return 0;
int leftLargest=0,rightLargest=0;
int left[]=new int[arr.length];
//left 数组中保存每个元素左边的最大值,left[i],表示数组中第i个元素的左边最大值。
int right[]=new int[arr.length];
//right数组中保存每个元素左边的最大值,right[i],表示数组中第i个元素的右边最大值。
for(int i=0;i<arr.length;i++)
{
leftLargest=Math.max(leftLargest,arr[i]);
left[i]=leftLargest;
}
//先遍历一次找出每个元素左边最大值。
for(int i=arr.length-1;i>=0;i--)
{
rightLargest=Math.max(rightLargest,arr[i]);
right[i]=rightLargest;
}
//遍历找到每个元素右边最大值。
for(int i=0;i<arr.length;i++)
{
water+=Math.min(left[i],right[i])>arr[i]?Math.min(left[i],right[i])-arr[i]:0;
}
//利用当前元素的左边最大值和右边最大值求得存水量。
return water;
}

上面算法的流程:
①从左到右遍历一次求出每个元素左边的最大值,保存在 left 数组中。
②从右到左遍历一次求出每个元素右边的最大值,保存在right数组中。
③最后一次遍历求出每个元素(每根柱子)的存水量。

改进解法2 :分析上面算法发现其实没有必要使用 left 数组,因为当从左到右遍历求存水量的过程中可以利用一个变量来保存当前元素左边的最大值。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static int water1(int []arr)
{
int water=0;
if(arr==null||arr.length<=1)
return 0;
int leftLargest=0,rightLargest=0;
int right[]=new int[arr.length];
//只用一个右数组来保存从右到左的最大值。
for(int i=arr.length-1;i>=0;i--)
{
rightLargest=Math.max(rightLargest,arr[i]);
right[i]=rightLargest;
}
for(int i=0;i<arr.length;i++)
{
leftLargest=Math.max(leftLargest, arr[i]);
//leftLargest 保存当前元素左边的最大值。
water+=Math.min(leftLargest,right[i])>arr[i]?Math.min(leftLargest,right[i])-arr[i]:0;

}
return water;
}

解法3

能不能在时间复杂度O(n),空间复杂度O(1)的情况下来完成存水量的问题,答案是肯定的,用几幅图来描述一下解法3的过程。数组为 { 5,2,3,2,4 }。

这里写图片描述

这里写图片描述

这里写图片描述

①上面左右两边的黄色块分别表示当前元素左边最大值和右边最大值。

②left ,right分别代表从左到右移动和从右到左移动的指针。

③如果当前元素的左边最大值比右边最大值小,则left指针向右移动,否则right指针向左移动。

④这种左右指针移动的目的是为了保证所求的左右最大值一定是当前元素的左右最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static int water3(int []arr)
{
int water=0;
if(arr==null||arr.length<=1)
return 0;
int largestLeft=0,largestRight=0;
//分别保存左右两边最大值
int left=0,right=arr.length-1;
while(left<right)
{
largestLeft=Math.max(arr[left], largestLeft);
largestRight=Math.max(arr[right], largestRight);
//获得左右两边最大值
if(largestLeft>largestRight)
{
//如果左边最大值大于右边则右指针向移动
water+=largestRight-arr[right--];
}
else {
//否则左指针向右移动
water+=largestLeft-arr[left++];
}
}

return water;
}
-------------本文结束感谢您的阅读-------------
undefined