LeetCode笔记-三数之和等于0

2,236 阅读8分钟

今天刷leetcode的时候做到的题目:

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

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

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

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

这个问题我采用

先排序,然后遍历的时候定一个基值两个左右移动控制和大小的指针

这种做法,但是让我困扰的是如何不重复。 最开始的思路是

定义一个int[],大小为给定数据nums里面最大值max和最小值min的差值 + 1,假设三数分别是a, b, c。当三数之和等于0时,根据

(int i) -> i < 0 : Math.abs(i) + max : i;

来生成数组对应的下标x, y, z。如果根据下标从数组中获取的三个值之和不等于3,则这个组合没有出现过。然后将三个下标对应的值赋值为1。

但是这里有问题了,当参数为int[] nums = {-4, -2, -2, -2, 0, 1, 2, 2, 2, 3, 3, 4, 4, 6, 6}时,前几个组合分别是[-4, -2, 6], [-4, 0, 4], [-4, 1, 3], [-4, 2, 2]。当下一个组合[-2, -2, 4]时对应的下标为8,8,4。而此时数组上8和4下标的值已经是1了,所以无法存入最终结果。

发现这个问题之后对他进行改造。

新定义存有效值的String[],它的大小等于nums的大小。在定义一个和它对应的有效下标indexP,每当前面保存坐标的int[]三数之和不等于3的时候将三个值转成String存入数组中并且indexP++。如果等于3,则遍历该String[],如果不存在该组合则加入最终组合,并将组合相连存入String[]

改造之后倒是通过了。最终代码如下

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        int n = nums.length;
        List<List<Integer>> result = new ArrayList<>();
        if (n < 3) {
            return result;
        }
        int[] data = new int[n];
        for (int i = 0; i < n; i++) {
            data[i] = nums[i];
        }
        Arrays.sort(data);
        
        int min = data[0];
        int max = data[n - 1];
        int[] index = new int[Math.abs(max - min) + 1];
        String[] index2 = new String[n];
        int indexP = 0;

        for (int i = 0; i < n; i++) {
            int a = data[i] < 0 ? Math.abs(data[i]) + max : data[i];
            int j = i + 1;
            int k = n - 1;
            while (j < k) {
                int sum = data[i] + data[j] + data[k];
                if (sum == 0) {
                    int b = data[j] < 0 ? Math.abs(data[j]) + max : data[j];
                    int c = data[k] < 0 ? Math.abs(data[k]) + max : data[k];
                    StringBuilder sb = new StringBuilder();
                    sb.append(a).append(b).append(c);
                    String p = sb.toString();
                    if ((index[a] + index[b] + index[c]) != 3) {
                        List<Integer> list = Arrays.asList(data[i], data[j], data[k]);
                        result.add(list);
                        index[a] = 1;
                        index[b] = 1;
                        index[c] = 1;
                        index2[indexP] = p;
                        indexP++;
                    } else {
                        if (!check(index2, p)) {
                            List<Integer> list = Arrays.asList(data[i], data[j], data[k]);
                            result.add(list);
                            index2[indexP] = p;
                            indexP++;
                        }
                    }
                    k--;
                    j++;
                } else if (sum > 0) {
                    k--;
                } else {
                    j++;
                }
            }
            indexP = 0;
        }
        return result;
    }

    static boolean check (String[] strs, String p) {
        int n = strs.length;
        int i = 0;
        while (strs[i] != null && i < n) {
            if (strs[i].equals(p)) {
                return true;
            }
            i++;
        }
        return false;
    }

}

此时虽然通过了,但是执行时间十分慢,需要O(n^2) ~ O(n^2 * n!),如果忽略最开始对传入参数的copy的话.(关于这点我在学习java函数式编程的时候要求方法的副作用尽量少或者没有,所以强迫式地copy了。)

该文章作为我自己的笔记记录大佬们对于该题的最快解法。提升姿势水平。 在leetcode上最快的代码如下:

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
      if (nums.length < 3)
        return Collections.emptyList();
      List<List<Integer>> res = new ArrayList<>();
      int minValue = Integer.MAX_VALUE;
      int maxValue = Integer.MIN_VALUE;
      int negSize = 0;
      int posSize = 0;
      int zeroSize = 0;
      for (int v : nums) {
        if (v < minValue)
          minValue = v;
        if (v > maxValue)
          maxValue = v;
        if (v > 0)
          posSize++;
        else if (v < 0)
          negSize++;
        else
          zeroSize++;
      }
      if (zeroSize >= 3)
        res.add(Arrays.asList(0, 0, 0));
      if (negSize == 0 || posSize == 0)
        return res;
      if (minValue * 2 + maxValue > 0)
        maxValue = -minValue * 2;
      else if (maxValue * 2 + minValue < 0)
        minValue = -maxValue * 2;

      int[] map = new int[maxValue - minValue + 1];
      int[] negs = new int[negSize];
      int[] poses = new int[posSize];
      negSize = 0;
      posSize = 0;
      for (int v : nums) {
        if (v >= minValue && v <= maxValue) {
          if (map[v - minValue]++ == 0) {
            if (v > 0)
              poses[posSize++] = v;
            else if (v < 0)
              negs[negSize++] = v;
          }
        }
      }
      Arrays.sort(poses, 0, posSize);
      Arrays.sort(negs, 0, negSize);
      int basej = 0;
      for (int i = negSize - 1; i >= 0; i--) {
        int nv = negs[i];
        int minp = (-nv) >>> 1;
        while (basej < posSize && poses[basej] < minp)
          basej++;
        for (int j = basej; j < posSize; j++) {
          int pv = poses[j];
          int cv = 0 - nv - pv;
          if (cv >= nv && cv <= pv) {
            if (cv == nv) {
              if (map[nv - minValue] > 1)
                res.add(Arrays.asList(nv, nv, pv));
            } else if (cv == pv) {
              if (map[pv - minValue] > 1)
                res.add(Arrays.asList(nv, pv, pv));
            } else {
              if (map[cv - minValue] > 0)
                res.add(Arrays.asList(nv, cv, pv));
            }
          } else if (cv < nv)
            break;
        }
      }
      return res;
    }
}

对于大佬的代码我是debug进行分析。

传入参数nums = [-4,-2,-2,-2,0,1,2,2,2,3,3,4,4,6,6]
使用工具vscode

首先是定义

      int minValue = Integer.MAX_VALUE;
      int maxValue = Integer.MIN_VALUE;
      int negSize = 0;
      int posSize = 0;
      int zeroSize = 0;

对于计算方面他定义了五个变量,分别是最大值、最小值、大于0的元素数目、小于0的元素数目、等于0的元素数目。

然后是第一个遍历

for (int v : nums) {
    if (v < minValue)
        minValue = v;
    if (v > maxValue)
        maxValue = v;
    if (v > 0)
        posSize++;
    else if (v < 0)
        negSize++;
    else
        zeroSize++;
}

这个遍历是为了对上述五个变量进行赋值。此时各个值为:

接着是if判断

if (zeroSize >= 3)
    res.add(Arrays.asList(0, 0, 0));
if (negSize == 0 || posSize == 0)
    return res;
if (minValue * 2 + maxValue > 0)
    maxValue = -minValue * 2;
else if (maxValue * 2 + minValue < 0)
    minValue = -maxValue * 2;

如果nums中0元素大于等于3个,则直接将[0, 0, 0]加入最终结果中。如果nums中都为0,则直接返回最终结果。如果最小值 * 2与最大值之和大于0则将最大值赋值为最小值的相反数 * 2,如果最大值 * 2与最小值之和小于0则将最小值赋值为最大值的相反数 * 2。这里的minValue和maxValue的作用我现在还看不出来,因此直接查看控制台的值:

我们可以看到对于该例各个数没有变化

int[] map = new int[maxValue - minValue + 1];
int[] negs = new int[negSize];
int[] poses = new int[posSize];
negSize = 0;
posSize = 0;

这里定义的map的长度为最大值和最小值的差值 + 1,以及定义两个int[],长度分别是nums中大于0和小于0的元素的个数,估计是用来保存对应的元素的,然后置0 negSize和posSize。

for (int v : nums) {
    if (v >= minValue && v <= maxValue) {
      if (map[v - minValue]++ == 0) {
        if (v > 0)
          poses[posSize++] = v;
        else if (v < 0)
          negs[negSize++] = v;
      }
    }
  }

这里第二次遍历,取出位于maxValue和minValue之间的元素,并且如果map中下标v - minValue的下一个元素为0,如果v大于0则poses的元素赋值为v然后下标右移。 此时上面定义的negs和poses的作用用来存符合条件的元素,而negSize和posSize的作用变为这两个数组的下标指针。 五个变量的值如下:

map的值如下:

negs的值:

poses的值:

这下对于上述代码的功能就知道了

它负责统计nums中重复元素的个数以及得到nums中所有不重复的值

Arrays.sort(poses, 0, posSize);
Arrays.sort(negs, 0, negSize);

这里将不重复元素按从小到大排列

int basej = 0;
for (int i = negSize - 1; i >= 0; i--) {
    int nv = negs[i];
    int minp = (-nv) >>> 1;
    while (basej < posSize && poses[basej] < minp)
      basej++;
    for (int j = basej; j < posSize; j++) {
      int pv = poses[j];
      int cv = 0 - nv - pv;
      if (cv >= nv && cv <= pv) {
        if (cv == nv) {
          if (map[nv - minValue] > 1)
            res.add(Arrays.asList(nv, nv, pv));
        } else if (cv == pv) {
          if (map[pv - minValue] > 1)
            res.add(Arrays.asList(nv, pv, pv));
        } else {
          if (map[cv - minValue] > 0)
            res.add(Arrays.asList(nv, cv, pv));
        }
      } else if (cv < nv)
        break;
    }
}

最后的遍历,由于不为0的数之和要等于0,其中一定会有两个数一个为负数一个为正数。

int basej = 0;
for (int i = negSize - 1; i >= 0; i--) {
    int nv = negs[i];
    int minp = (-nv) >>> 1;
    while (basej < posSize && poses[basej] < minp)
      basej++;
      /* other code */
}

这里的遍历顺序是以负数开始遍历,并且按照从大到小遍历,获取到最大的符合(-nv) >>> 1条件的正数下标。

for (int j = basej; j < posSize; j++) {
    int pv = poses[j];
    int cv = 0 - nv - pv;
    /* other code */
}

这里的pv是获取到的符合条件的最小正数,而cv是为了三数之和为对应条件的剩下的值。

if (cv >= nv && cv <= pv) {
  if (cv == nv) {
    if (map[nv - minValue] > 1)
      res.add(Arrays.asList(nv, nv, pv));
  } else if (cv == pv) {
    if (map[pv - minValue] > 1)
      res.add(Arrays.asList(nv, pv, pv));
  } else {
    if (map[cv - minValue] > 0)
      res.add(Arrays.asList(nv, cv, pv));
  }
} else if (cv < nv)
  break;

如果最后的值小于负数则退出该负数的遍历进行下一个,因为之后的遍历可能会用到。

if (cv == nv) {
    if (map[nv - minValue] > 1)
      res.add(Arrays.asList(nv, nv, pv));
  } else if (cv == pv) {
    if (map[pv - minValue] > 1)
      res.add(Arrays.asList(nv, pv, pv));
  } else {
    if (map[cv - minValue] > 0)
      res.add(Arrays.asList(nv, cv, pv));
  }

最后根据第三个值是否需要和前两个数的其中一个重复以及对应元素的个数来获取最终结果。如果不相等则直接获取最终结果。

总结

大佬代码的思路:

分别获取nums中不重复的正负元素集合以及这些元素对应的个数,从最大的负数开始遍历,先获取元素 * 2 小于负数的最大的正数,然后开始遍历正数。然后根据最终结果计算出还欠缺的第三个数,如果存在且满足一定条件则将这个组合加入最终结果中。

大佬代码的优点:

将最终对三个元素的获取的遍历范围限定在最小区间内,完全不需要遍历所有元素,只需要遍历0左右其中一个区间的元素即可,而且在得到第一个值要获取第二个值的时候还对第二个值的区间进一步地收缩:poses[basej] < minp,以此达到最快的遍历速度.

大佬代码的特点:

在代码阶段就对要进行遍历的数的区间进行限定而不是执行的时候根据条件判断进行,大大减少了遍历的次数