【算法练习题】力扣练习题——数组(2):三数之和

原题说明

给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。

注意:答案中不可以包含重复的三元组。

例如, 给定数组 nums = [-1, 0, 1, 2, -1, -4],

满足要求的三元组集合为:
[[-1, 0, 1],[-1, -1, 2]]

原题链接:https://leetcode-cn.com/problems/3sum


解法一:基于HashMap的暴力求解

参考力扣题:https://leetcode-cn.com/problems/two-sum/

可以先固定三数中的一个,然后对剩下的两数进行一次遍历。时间复杂度应该是$O\left(\mathrm{n}^{2}\right)$

但是不同于两数加和题,本题有一个难点,由于要求输出所有组合,因此需要避免重复情况

我的初步想法是

  1. 找到满足条件的三数;
  2. 对三数进行排列;
  3. 将三数组合转化成字符串;
  4. 将其存储到容器中;
  5. 通过容器特性进行筛选;

 第一步:对于查找部分,声明了HashMapmaptwo用于查找一个数i固定后的其余两数。

该部分代码(查找代码)如下:

1 for(int j = i+1;j<nums.length;j++) {                
2     int target = sum - nums[j];        
3     if(maptwo.containsKey(target)) {//used for judging the sum                    
4         int k = maptwo.get(target);                        
5             sumlist.add(new ArrayList<Integer>(Arrays.asList(nums[i], nums[j], nums[k])));                    
6     }
7     maptwo.put(nums[j], j);
8 }    

第二步:对于排列部分,直接使用了Array对象自带的排序函数 

int[] newnums = {nums[i],nums[k],nums[j]};
Arrays.sort(newnums);

排序的原因是,防止出现$-101$与$-110$这样从字符串的变量类型看不同、实际是相同的情况。

这里可能要注意的是,一开始我曾尝试在获得输入数组时,先排序,后期就不用再次排序。但是后来调试的时候,发现由于HashMap的缘故,得到的三数顺序仍然会变。举个例子,对于$-1,0,1$,在查找时,会先找到$0$,但是由于算法的缘故,此时这个数只是被存入maptwo、不做最后的存储,直到找到$1$。此时nums[j]应该是$1$,而nums[k]却是$0$,因此需要重新排序。

第三步:将满足条件的三数转化成字符串

这里使用的数据类型是StringBuffer,这样可以动态添加数组。

sb.append(String.valueOf(newnums[0]));
sb.append(String.valueOf(newnums[1]));
sb.append(String.valueOf(newnums[2]));

添加的位置。一开始我选择添加的位置是得到每一个元素,比如上个代码段的第2行和第3行添加nums[j],第4行之后添加nums[k]。有个问题要注意:在真正满足条件的第三个数出现时,第二个数和第三个数之间所有的数都会被当做nums[j]添加到StringBuffer中。举个例子,对于给定的数组$-2,-1,-1,0,1,3$,在组合$-1,3$中的所有数都会被添加。因此应该要在确认第三个数之后进行字符串转化和添加操作。

第四步&&第五步:存储到容器中并筛选

声明了HashMapmapall负责存储字符串信息并进行筛选。代码如下:

if(!mapall.containsValue(sb.toString())) {//used for judging whether repeated						
	mapall.put(h++, sb.toString());	//sb is the type of StringBuffer					
} 
sb = new StringBuffer();

后来注意到,其实没有必要用HashMap的。

在这里,由于兼具筛选的功能,所有最后的对三数组合的存储应该放在这段代码中包裹起来。另外,StringBuffer也应该在存储到HashMap后被释放。

以下是完整的代码:

 1 public ArrayList<ArrayList<Integer>> threeSum(int[] nums) {
 2     if(nums.length < 3 || nums == null) {
 3         return null;
 4     }
 5     
 6     ArrayList<ArrayList<Integer>> sumlist = new ArrayList();    
 7     
 8     HashMap<Integer, String> mapall = new HashMap<Integer, String>();
 9     int h = 0;
10 
11     for(int i = 0; i<nums.length -1; i++) {
12         //used for search nums of the left two elements
13         HashMap<Integer, Integer> maptwo = new HashMap<Integer, Integer>();
14         //used for comparing the repeated combination
15         StringBuffer sb = new StringBuffer();
16         
17         int sum  = 0-nums[i];
18         for(int j = i+1;j<nums.length;j++) {                
19             int target = sum - nums[j];        
20             if(maptwo.containsKey(target)) {//used for judging the sum                    
21                 int k = maptwo.get(target);
22                 
23                 int[] newnums = {nums[i],nums[k],nums[j]};
24                 Arrays.sort(newnums);
25                 sb.append(String.valueOf(newnums[0]));
26                 sb.append(String.valueOf(newnums[1]));
27                 sb.append(String.valueOf(newnums[2]));
28                 
29                 if(!mapall.containsValue(sb.toString())) {//used for judging whether repeated                        
30                     sumlist.add(new ArrayList<Integer>(Arrays.asList(nums[i], nums[j], nums[k])));
31                     mapall.put(h++, sb.toString());                        
32                 }
33                 sb = new StringBuffer();
34             }
35             maptwo.put(nums[j], j);
36         }
37     }
38     return sumlist;
39 }

最后失败在了求解时间上。。。

解法二:双指针

时间复杂度上,该解法和上述解法一致。同样需要先排序(其实解法一可能不需要先排序?)

原理是,固定最左端的数字。剩下的数值作为两个数的遍历空间。两个数分别在区域的两端,设为LR

遍历的方式为

$\operatorname{sum}=n u m s[i]+n u m s[L]+n u m s[R]\left\{\begin{array}{l}{=0, \text { done }} \\ {>0, R--} \\ {<0, L++}\end{array}\right.$

图解为

找到满足条件的三值后,采用和解法一相同的方式存入数值

然后防止重复的方法为,

对于,若$\operatorname{nums}[L]==\operatorname{nums}[L++]$,则$L++$。如图,使得左端数遇到重复数值时,选择角标最大的(跳过左边的数),不至于重复。

 

代码实现为

while(L<R && nums[L]==nums[L+1]) L++;

同理,若$\operatorname{nums}[R--]==\operatorname{nums}[R]$,则$R--$,如图,使得右端数遇到重复数值时,选择角标最小的(跳过右边的数),不至于重复。

 

代码实现为

while(L<R && nums[R]==nums[R-1]) R--;

这里需要注意的问题有两处:

  • 两行代码都应该在确认三数加和满足要求的情况下,否则,直接跳过会缺失解。
  • 循环判定条件务必不能遗漏L<R(也是第二层循环的循环条件)。反例是$-2,1,1,1,1$,如此一来,$L$会一直增加、直到数组越界。

之后是第一层循环的迭代。这里注意的是防止重复,原理和左端数值时一样的。只是我在写代码的时候,被边界条件困住了。这里先给出整体的代码:

 1 public ArrayList<ArrayList<Integer>> newSolution (int[] nums) {
 2     ArrayList<ArrayList<Integer>> sumlist = new ArrayList();    
 3     
 4     if(nums.length < 3 || nums == null) {
 5         return sumlist;
 6     }
 7     
 8     Arrays.sort(nums);
 9     
10     int i = 0, L = i + 1, R = nums.length - 1;
11     while(i<nums.length && nums[i]<=0) {
12         while(L<R) {                
13             int sum = nums[i]+nums[L]+nums[R];
14             if(sum==0) {
15                 sumlist.add(new ArrayList<Integer> (Arrays.asList(nums[i],nums[L],nums[R])));
16                 while(L<R && nums[L]==nums[L+1]) L++;//in case the left element is repeat
17                 while(L<R && nums[R]==nums[R-1]) R--;
18                 L++;R--;
19             }
20             else if(sum<0){//means the left element is bigger
21                 L++;
22             }
23             else if(sum>0) {//means the right element is bigger
24                 R--;
25             }
26         }
27         while(i+1<nums.length && nums[i+1]==nums[i]){//in case it is repeat,
28             i++;
29         }
30         i++;//make the iteration run
31         L=i+1;
32         R=nums.length-1;
33     }
34     return sumlist;
35 }

一开始我没有考虑第二个i++使得循环跑不起来。

后来在循环的判定条件上没有加入i+1<nums.length ,这就使得数组越界。

对于L也不是没有担心。后来发现,直接在第一层循环处,加入i的循环判定条件就好了。


总结:

这道题给我折腾坏了。

  • 这些内容都不难,只是实现的过程,代码之间的相关关系让人很头疼。所以以后实现的顺序、也就是逻辑一定分清楚。
  • 另外第一次遇到运算问题的麻烦,解法二的完整代码中,第15行、第16行代码,一开始是将L<R的条件放在后面的,所以就总是数组越界,我当时也没太明白,后来才知道交集运算是从左往右算,所以一看到数组越界,程序执行就报错了。
  • 最后第27行到第30行的代码,我鼓捣了得有半个小时,就是总越界?后来冷静想想,这玩意儿有啥呀,很简单。就是我对每个步骤的意义没有明确。比如说好几次都是删除了第30行代码,可是这个就是防止上面循环跑不动了才存在的。循环部分也是不断鼓捣,有一些人用的判定条件是$n u m s[i]==n u m s[i-1]$,为了达到这个目的,我就用了do-while语句,一通折腾、导致边界条件非常混乱。其实我觉得几种语句都是类似的,无非是执行的逻辑不一样,所以自己撸代码的时候还是应该就自己的情况,明确自己的边界条件。

猜你喜欢

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