原题说明:
给定一个包含 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)$
但是不同于两数加和题,本题有一个难点,由于要求输出所有组合,因此需要避免重复情况
我的初步想法是
- 找到满足条件的三数;
- 对三数进行排列;
- 将三数组合转化成字符串;
- 将其存储到容器中;
- 通过容器特性进行筛选;
第一步:对于查找部分,声明了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 }
最后失败在了求解时间上。。。
解法二:双指针
时间复杂度上,该解法和上述解法一致。同样需要先排序(其实解法一可能不需要先排序?)
原理是,固定最左端的数字。剩下的数值作为两个数的遍历空间。两个数分别在区域的两端,设为L和R。
遍历的方式为
$\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语句,一通折腾、导致边界条件非常混乱。其实我觉得几种语句都是类似的,无非是执行的逻辑不一样,所以自己撸代码的时候还是应该就自己的情况,明确自己的边界条件。
原文地址:https://www.cnblogs.com/RicardoIsLearning/p/12028069.html