【算法练习题】力扣数组练习题(1):盛最多水的容器

原题说明

给定 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器,且 n 的值至少为 2。


原题链接:https://leetcode-cn.com/problems/container-with-most-water


 解法一:暴力求解

拿到这道题的第一个想法就是暴力求解。设定两层循环,第一层用于遍历容器的左边高度、第二层用于遍历容器的右边高度。然后确定题解的不定式就是(容器面积)大小的比较。

给定表示容器边的高度数组是int[] height,我的思考过程是:

1. 两层循环的循环变量分别是pre(左边容器高度)以及post(右边容器高度)

2. 两个循环变量的边界分别是

pre<height.length-1
post<height.length

3. 确定不定式为容器面积大小的比较

maxsize = Math.max(maxsize, (Math.min(height[pre], height[post]))*(post-pre));    	

4.再每次循环结束前,循环变量迭代。特别注意,在这里右边高度是在左边高度的基础上迭代

以上分析完成后,就可以写解法,如下:

 1 public int maxSize(int[] height) {
 2     if(height.length < 2 || height == null)
 3         return 0;
 4     
 5     int pre = 0, post = 1;
 6     int maxsize = 0;
 7     while(pre<height.length-1) {
 8         while(post<height.length) {
 9             maxsize = Math.max(maxsize, (Math.min(height[pre], height[post]))*(post-pre));            
10             post++;
11         }          
12         pre++;
13         post = pre + 1;
14     }
15     return maxsize;
16 }

第一遍调试的时候,就发生两个错误:

1)第9行代码在进行左右两边高度比较是,用得是角标而不是高度值;

2)没有考虑第4点、也就是第13行代码,使得损失了一些容边边长组合。

 

解法二:试图优化的解法(失败了)

很容易想到本题应该是双指针是相关的。又本着两个指针都是从数组左端向右端遍历的惯性思维,当时的想法就是考虑合适的情况,在右指针一次遍历的时候,左指针根据面积大小比较的结果也跟着变化。

当时我考虑了三种情况:

1)新遍历的容器面积

int snew = (Math.min(height[pre], height[post]))*(post-pre);

2)考虑左指针相邻边的容器面积

int k = (height[pre+1]>height[pre])? (pre+1): pre;//for the next
int snext = (Math.min(height[k], height[post]))*(post-k);   

3)考虑左右指针范围内最高边(作为左指针)的容器面积

j = (height[post]>height[j])? post: j;//for the highest
int shigh = (Math.min(height[j], height[post]))*(post-j);  

 然后通过比较上述三种情况与上一步的容器面积大小,进行左指针的迭代。

if(size == snext && k != pre) {pre++;}
if(size == shigh && j != pre) {pre=j;}

考虑第(2)种情况,当时是觉得可以正常代替暴力求解的外循环,但是这里当遇到的问题就是,要是相邻的元素高度是相等的,采用取大值来试图迭代是要失败的;

于是考虑到第(3)种情况,即从右指针已经遍历的范围内挑出最大值作为边长,这种情况在我当时看来,应该是对一种补充。(现在想来,要是有情况(3),就不需要情况(2)了)

因为对于这题的理解,我的想法是,右指针的遍历已经能够确保,容器右边涉及的情况都被包含了。而左边只需要找到当前左右指针遍历的范围内的最高边长度,就可以保证容器面积最大。

解法如下:

 1 public int maxSize(int[] height) {
 2     if(height.length<2 || height == null)
 3         return 0;
 4     
 5     int pre = 0;
 6     int post = height.length-1;
 7     int maxsize = 0;
 8     while(pre<post) {
 9         int newsize = Math.min(height[pre], height[post])*(post-pre);
10         maxsize = Math.max(maxsize, newsize);
11         if(height[pre]>height[post])
12             post--;
13         else
14             pre++;
15     }
16     return maxsize;
17 }

后来在测试样例[75,21,3,152,13,107,163,166,32,160,41,131,7,67,56,5,153,176,29,139,61,149,176,142,64,75,170,156,73,48,148,101,70,103,53,83,11,1...]时报错了。我猜测原因,只可能是遍历的情况不完整。

解法三:双指针(参考答案后,自己的解法,失败了)

官方解答的双指针是从数组两端向中间遍历的。不过我最担心的是遍历的情况是否一遍就可以访问所有的情况?

我自己的想法是,还是要从每次指针移动的情况来判断。以左指针为例,如果左指针+1的数组元素值(高度)小于等于左指针的数组元素值,考虑到整个容器的宽度还因此减小,必然不可能比原来的面积更大。

因此,但凡指针移动,面积一定会变大,直到两端指针向中间遍历所有数组元素。但是在这种情况下,发现指针根本不动......后来想了想,还是因为数组元素可能重复,而使得指针不移动。再次印证大小比较来移动在此处行不通

代码如下:

 1 public int solution4(int[] height) {
 2     if(height.length<2 || height == null)
 3         return 0;
 4     
 5     int pre = 0;
 6     int post = height.length-1;
 7     int maxsize = Math.min(height[pre], height[post])*(post-pre);
 8     while(pre<post) {
 9         int leftheight = Math.max(height[pre], height[pre+1]);
10         pre = (height[pre]<height[pre+1])?(pre+1):pre;
11         int rightheight = Math.max(height[post-1], height[post]);
12         post = (height[post]<height[post-1])?(post-1):post;
13         int newsize = Math.min(leftheight, rightheight)*(post-pre);
14         
15         maxsize = Math.max(maxsize, newsize);        
16     }
17     return maxsize;
18 }

解法四:双指针(标准答案)

先贴上官方的解法:

 1 public int solution3(int[] height) {
 2     if(height.length<2 || height == null)
 3         return 0;
 4     
 5     int pre = 0;
 6     int post = height.length-1;
 7     int maxsize = 0;
 8     while(pre<post) {
 9         int newsize = Math.min(height[pre], height[post])*(post-pre);
10         maxsize = Math.max(maxsize, newsize);
11         if(height[pre]>height[post])
12             post--;
13         else
14             pre++;
15     }
16     return maxsize;
17 }

 解法二之所以会考虑这么细,是因为很担心出现对双指针的遍历分析不够,认为总会漏掉某些组合。解法三又是因为一开始觉得官方的解法,只是比较左右指针的元素数值,感觉一定缺乏分析?

后来看了另一个题解,发现这个思路是完备的。

定义左指针为i,右指针为j,每种情况是(i,j)作为容边的两边

那么一共是两种情况,三个考虑节点

情况(1)就是初始情况

情况(2)是指针移动,这里有两个节点、即左指针移动和右指针移动(本质是一样的)——

先考虑左指针,

如果$h(i)<h(j)$,那么设左右指针范围内的任一节点k,其组合$X = \{ (i,k)|i < k \le j\} $都是可以排除的。

给定所有的情况为

 $\forall(i, k) \in X, H(i, k)=\left\{\begin{array}{l}{h(i) *(k-i), h(i) \leq h(k)} \\ {h(k) *(k-i), h(i)>h(k)}\end{array}\right.$

对于$h(i)<h(k)$而言,如图,则有

$h(i) *(k-i) \leq h(i) *(j-i)$(蓝色面积小于红色面积)

对于$h(i)>h(k)$而言,如图,则有

$h(k) *(k-i) \leq h(i) *(k-i)<h(j) *(j-i)$(蓝色面积小于红色面积)

综上,在$h(i)<h(j)$,$h(j) *(j-i)$已经包含/排除了其它所有的组合情况,因此只需要考察$h(i+1) ? h(j)$的情况即可,即可以安全地迭代$i$

尽管$i$迭代后的面积可能更小,但是通过面积大小的比较可以让算法继续遍历。

对于右指针,情况相同。故通过左右指针从两边向中间遍历,可以包含所有的组合情况。


 总结:

  • 从数组两端向中间遍历的双指针是解法的基础
  • 通过证明遍历的情况下能够包含所有可能,是双指针安全的根据

猜你喜欢

转载自www.cnblogs.com/RicardoIsLearning/p/12015085.html