zoukankan      html  css  js  c++  java
  • No.015:3Sum

    问题:

    Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0?
    Find all unique triplets in the array which gives the sum of zero.
    Note: The solution set must not contain duplicate triplets.
    For example, given array S = [-1, 0, 1, 2, -1, -4],
    A solution set is:
    [
      [-1, 0, 1],
      [-1, -1, 2]
    ]

    官方难度:

    Medium

    翻译:

    给定一个长度为n的无序数组S,是否存在三个元素a,b,c,使得a+b+c=0?找出所有的可能解。

    注意解集中不能存在重复解。

    例子:

    数组S:{-1,0,1,2,-1,-4}。

    解集:[ [-1,0,1],[-1,-1,2] ]

    方法一:

    1. 这是No.001(2Sum)问题的深入讨论。
    2. 在2Sum问题中,使用了哈希表的策略,但是有一个前提:2Sum问题中的数组是无序的。在无序数组中使用哈希表能大大提高效率,这是基于给2Sum问题的数组执行时间复杂度为O(logn)排序操作并不值得,但是3Sum问题没有这个顾虑,优先执行排序操作可以极大简化算法的性能。
    3. 由于要求解集不能存在重复解,所以要在思考到各种可能性。这里提一句:想要使用Set集合的“去重”特性取巧的行为,并行不通。因为在Set集合中存放类似数组或是集合,只要不是指向同一个地址,否则哪怕数据完全一样,都不会触发“去重”的特性。如下例子的输出是:2。
    4.     public static void main(String[] args) {
              Set<List<Integer>> result = new HashSet<>();
              List<Integer> list1 = new ArrayList<>();
              list1.add(1);
              List<Integer> list2 = new ArrayList<>();
              list1.add(1);
              result.add(list1);
              result.add(list1);
              result.add(list2);
              System.out.println(result.size());
          }
    5. 集合有List.contains()方法,判断集合中是否存在某个元素;而在数组中,也有Arrays.binarySearch()方法,可以在已经排序过的数组中达到这个目的,方法的返回值是寻找元素在数组中的下标索引,如果数组中没有这项,会返回一个负值(实际上是,如果存在这个数,在数组中的位置的相反数-1,如在数组[1,2,4]中查找3,数组[1,2,3,4]中元素3的索引2,所以方法的返回值是-2-1=-3)。这个方法的原理,是二分查找法,有较高的效率。
    6. 基本思想如下:从数组首位开始循环,直到数组的倒数第三项,将问题转化为,在剩下的已排序数组中,寻找第一个数的相反数的2Sum问题。这个2Sum问题的解集,可能有多个。
    7. 从数组剩余项的第一位开始循环,在接下来的数组中寻找指定值:前两个数的相反数。使用Arrays.binarySearch()方法,但是这个方法的入参中的数组长度要尽可能的缩小:从第j+1项开始,到lengthEnd结束。其中lengthEnd的值在开始的时候是数组最后一项的下标索引值,但是,如果在内循环中,出现了一次匹配之后,会更新这个值lengthEnd=index。因为在nums[j]越来越大的情况下,在前一次匹配中获得的index,其之后的值比nums[index]更大,没有考虑的必要。
    8. 注意得到的3个值,在数组中的下标依次是i,j,j+1+index。而且一定会有:nums[i]<=nums[j]<=nums[j+1+index]。
    9. 思考在Set集合不能去重的情况下,怎么保证解集没有重复?无论在外循环还是内循环中,分别维护上一个值previous,如果当前值等于previous,直接continue跳出本次循环。
    10. 有没有特殊情况,在循环进行到某一时刻,再也没有实现目标的机会,我可以直接跳出循环?答案是有的。在外循环中,如果当前项nums[i]>0,那么剩余两项比nums[i]更大,自然不可能达到三数之和为0的目标。同理,在内循环中,如果当前项nums[j]>-nums[i],也可以直接break。
    11. 最后考虑一点,返回值的类型被规定为List<List<Integer>>,用List的哪一个实现类比较好呢?鉴于整个方法,只要用到List.add()方法,不需要去读取数据,那么基于链表实现的LinkedList是否比基于数组实现的ArrayList,有更好的效率呢?

    方法一的解题代码:

     1     private static List<List<Integer>> method(int[] nums) {
     2         List<List<Integer>> result = new LinkedList<>();
     3         // 先给数组排序
     4         Arrays.sort(nums);
     5         int previous = 1;
     6         for (int i = 0; i < nums.length - 2; i++) {
     7             // 第一个数大于0,之后更加没机会了
     8             if (nums[i] > 0) {
     9                 return result;
    10             }
    11             // 如果和上个值相同,直接跳过
    12             if (nums[i] == previous) {
    13                 continue;
    14             }
    15             // 下次探寻终点
    16             int lengthEnd = nums.length;
    17             int previous2 = Integer.MIN_VALUE;
    18             for (int j = i + 1; j < lengthEnd - 1; j++) {
    19                 // 内循环第一个数,要大于0-array[i]
    20                 if (nums[j] > -nums[i]) {
    21                     break;
    22                 }
    23                 if (nums[j] == previous2) {
    24                     continue;
    25                 } else {
    26                     previous2 = nums[j];
    27                 }
    28                 // 定下两个,其实下一个也就决定了
    29                 int numberToFind = -(nums[i] + nums[j]);
    30                 // Arrays.binarySearch()方法只能在有序数组中使用
    31                 // 注意不是在整个数组上用这个方法,是在剩余数组上(夹逼过的)
    32                 int[] remian = Arrays.copyOfRange(nums, j + 1, lengthEnd);
    33                 int index = Arrays.binarySearch(remian, numberToFind);
    34                 // Arrays.binarySearch()查找失败时,会返回一个负值
    35                 // JAVA_API:这保证了当且仅当此键被找到时,返回的值将 >= 0。
    36                 if (index >= 0) {
    37                     // 因为有序,下一个array[j]一定比现在的大或等于(有去重要求)
    38                     // 而index之后的肯定比array[index]大,再之后的就没有必要遍历了
    39                     // 注意+1,因为lengthEnd是指长度而不是下标
    40                     // 再注意,这里的index,是去掉原数组前j项的index,加回去
    41                     lengthEnd = index + 1 + j;
    42                     // 这里可以保证array[i]<=array[j]<=array[index],不必担心Set集合接收重复问题
    43                     List<Integer> list = new LinkedList<>();
    44                     list.add(nums[i]);
    45                     list.add(nums[j]);
    46                     list.add(nums[index + j + 1]);
    47                     result.add(list);
    48                 }
    49             }
    50             previous = nums[i];
    51         }
    52         return result;
    53     }
    method

    方法二:

    1. 如果在内循环中,只有一个解,我承认使用Arrays.binarySearch()的效率会很高,但实际上并不然。内循环中,寻找数组中2个值的和为给定值,这个问题的解可能是多个的,虽然我已经极力缩小二分查找的数组范围,但显然,应该存在更优的方法。
    2. 在内循环中,从数组两端开始考虑,根据nums[left]+nums[right]和-nums[i]的大小比较,不断地增加left,或是减小right。这种不断夹逼的方式,和循环使用二分查找相比,显然前者效率更高。
    3. 其余的关键点,方法一中都有提及,唯一不同的是,这次内循环中需要维护preLeft和preRight两个值。
    4. 在一次偶然的情况下,我将内循环判断结束的条件:nums[left]>-nums[i]中的执行语句,写成了return result,而不是break。惊讶地发现,结果竟然是正确的,这引发了我的思考,因为如此一来,外循环中判断nums[i]>0的条件,其实是可以省略的。以一个很大的数组为例,当外循环的第一项,执行到接近于0的负数时,内循环整体的趋势是right-1,left的值是不会变的,当left增加到大于-nums[i]的情况,那么即使nums[i]之后还有负数,也没有3数之和为0的可能性了。

    方法二的解题代码:

     1     public static List<List<Integer>> threeSum(int[] nums) {
     2         if (nums == null || nums.length < 3) {
     3             throw new IllegalArgumentException("Input error");
     4         }
     5         List<List<Integer>> result = new LinkedList<>();
     6         Arrays.sort(nums);
     7         int previous = 1;
     8         for (int i = 0; i < nums.length - 2; i++) {
     9             if (nums[i] == previous) {
    10                 continue;
    11             } else {
    12                 previous = nums[i];
    13             }
    14             int left = i + 1;
    15             int right = nums.length - 1;
    16             int numberToFind = -nums[i];
    17             // 维护上一个左值和右值
    18             int preLeft = Integer.MIN_VALUE, preRight = Integer.MIN_VALUE;
    19             // 夹逼剩余两数之和
    20             while (left < right) {
    21                 if (nums[left] > -nums[i]) {
    22                     return result;
    23                 }
    24                 if (nums[left] == preLeft) {
    25                     left++;
    26                     continue;
    27                 }
    28                 if (nums[right] == preRight) {
    29                     right--;
    30                     continue;
    31                 }
    32                 int sum = nums[left] + nums[right];
    33                 // 找到不能退出循环,还有其他可能性
    34                 if (sum == numberToFind) {
    35                     List<Integer> list = new LinkedList<>();
    36                     list.add(nums[i]);
    37                     list.add(nums[left]);
    38                     list.add(nums[right]);
    39                     result.add(list);
    40                 }
    41                 if (sum < numberToFind) {
    42                     preLeft = nums[left];
    43                     left++;
    44                 } else {
    45                     preRight = nums[right];
    46                     right--;
    47                 }
    48             }
    49         }
    50         return result;
    51     }
    threeSum

    相关链接:

    https://leetcode.com/problems/3sum/

    https://github.com/Gerrard-Feng/LeetCode/blob/master/LeetCode/src/com/gerrard/algorithm/medium/Q015.java

    PS:如有不正确或提高效率的方法,欢迎留言,谢谢!

  • 相关阅读:
    作业4.称体重
    一、虚拟环境.二、路由配置主页与404.三、2.x路由分发.四、伪静态.五、request对象.六、FBV与CBV.七、文件上传.
    一、数据库表中字段的增删改查,二、路由基础.三、有名无名分组.四、多app共存的路由分配.五、多app共存时模板冲突问题.六、创建app流程.七、路由分发.八、路由别名,九、名称空间.十、反向解析.十一、2.x新特性.十二、自定义转换器
    Django项目的创建与介绍.应用的创建与介绍.启动项目.pycharm创建启动项目.生命周期.三件套.静态文件.请求及数据.配置Mysql完成数据迁移.单表ORM记录的增删改查
    学习Django,http协议,
    值类型之间的相互转化,运算符,if条件判断,循环,函数
    js导读,js引入,js选择器,事件,操作页面文档,计算后样式,数据类型
    字体图标,盒子显隐,overflow属性,伪类设计边框,盒子阴影2d形变
    浮动布局,定位布局(固定定位,绝对定位,相对定位),过渡动画
    盒子总结,文本属性操作,reset操作,高级选择器,高级选择器优先级,边界圆角(了解),a标签的四大伪类,背景图片操作,背景图片之精灵图
  • 原文地址:https://www.cnblogs.com/jing-an-feng-shao/p/5955453.html
Copyright © 2011-2022 走看看