zoukankan      html  css  js  c++  java
  • 【力扣算法】数组(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/

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

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

    我的初步想法是

    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{egin{array}{l}{=0, ext { done }} \ {>0, R--} \ {<0, L++}end{array} ight.$

    图解为

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

    然后防止重复的方法为,

    对于,若$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++ 第30行)使得循环跑不起来。

    后来在循环的判定条件上没有加入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语句,一通折腾、导致边界条件非常混乱。其实我觉得几种语句都是类似的,无非是执行的逻辑不一样,所以自己撸代码的时候还是应该就自己的情况,明确自己的边界条件。
  • 相关阅读:
    【学习笔记】JS知识点整理
    x86汇编语言实践(3)
    【小知识点】网页的链接跳转
    数据库系统内幕阅读笔记-第一部分
    【MySQL】06-排序
    【MySQL】05-锁
    【Py】Python基础——杂七杂八的用法
    【MySQL】04-索引
    【MySQL】03-事务
    【MySQL】02-更新流程
  • 原文地址:https://www.cnblogs.com/RicardoIsLearning/p/12028069.html
Copyright © 2011-2022 走看看