Difficulty: Medium

这两道题其实是一个类型,都是TwoSum的变种。只要在外部循环里嵌套进TwoSum就好了。

SumTwo 总结

我们首先总结一下TwoSum的两种方法的优劣。两种方法:一种就是(对于有序元素)用双指针,对于重复元素可以快速跳过。另一种就是用hashmap(不要求有序)记录出现过的元素。它们都是线性复杂度。

我们分两种情况考虑:

情况一:只求一个解

如果天生有序,显然前一种方法更好(Two Sum II),因为可能跳过元素加速,当然两种方法都是线性复杂度。如果无序,前一方法还要先排序,那么后一种方法就更好(TwoSum)。

如果要求的是输出结果在原数组中的位置,前一种方法可备份原数组,最后从原数组里查找。后一种方法使用unordered_map可直接记录(在数字可以重复的时候,返回的是原数组中的任一一个含有此数字的位置)。这对复杂度都不会有影响。

使用hashmap的时候,我们既可以渐进地向set里添加元素,每加一个元素之前都先找找set里有没有满足要求的另一半。我们也可以使用map(multiset)来记录每个元素出现的次数,然后依次对map中的元素找另一半。

情况二:要求所有解

这时候就必须去重。前一种方法有序输出,自带去重。后一种方法就要用RB-tree(vector不可被hash,所以不可以用hashmap)这反而会带来logn的复杂度。

对于无序输入,两种方法复杂度上无本质区别。但前一种方法可跳过重复元素,这在某些情况下会有优势(3Sum)。这时候如果要返回在原数组中的位置,因为数字可以重复,建立映射就比较麻烦了。

对于有序输入,显然前一种方法复杂度更低。

总结

总之,只有在无序且只求一解的情况下,采用hashmap有优势。

在使用双指针时,外层循环会有一个while(i<nums.size()-2)的逻辑,但一定要注意size_t与int做减法时可能会溢出。

3Sum Solution 1:

这道题要求的是所有解,一开始我没想清楚,想通过set来去重,但最后超时了。

class Solution {
  public:
    set < vector < int >> res;

  void sum2(vector < int > & nums, int i) {
    int target = -nums[i];
    unordered_set < int > s;
    for (int j = i + 1; j < nums.size(); j++) {
      if (s.count(target - nums[j])) {
        int a = nums[i];
        int b = nums[j];
        int c = target - nums[j];
        int m = min(min(a, b), c);
        int M = max(max(a, b), c);
        res.insert({
          m,
          -m - M,
          M
        });
      }
      s.insert(nums[j]);
    }
  }

  vector < vector < int >> threeSum(vector < int > & nums) {
    if (nums.size() < 3)
      return {};
    int i = 0;
    while (i < nums.size() - 2) {
      sum2(nums, i);
      i++;
      while (i < nums.size() - 2 && nums[i] == nums[i - 1]) i++;
    }
    return vector < vector < int >> (res.begin(), res.end());
  }
};

3Sum *Solution 2:

如前文所说,这就属于输入无序,求所有解的情况,应该先排序,再使用双指针。

class Solution {
  public:
    vector < vector < int >> res;
  vector < vector < int >> threeSum(vector < int > & nums) {
    sort(nums.begin(), nums.end());
    int i = 0;
    while (i < ((int) nums.size()) - 2) { ///
      if (nums[i] > 0) return res;
      check(nums, i);
      i++;
      while (i < nums.size() - 2 && nums[i] == nums[i - 1]) i++;
    }
    return res;
  }
  void check(vector < int > & nums, int i) {
    int target = -nums[i];
    int j = i + 1;
    int k = nums.size() - 1;
    while (j < k) {
      int sum = nums[j] + nums[k];
      if (sum > target) {
        k--;
        while (j < k && nums[k] == nums[k + 1]) k--;
        if (nums[k] < 0) return;
      } else if (sum < target) {
        j++;
        while (j < k && nums[j] == nums[j - 1]) j++;
      } else {
        res.push_back({
          nums[i],
          nums[j],
          nums[k]
        });
        k--;
        while (j < k && nums[k] == nums[k + 1]) k--;
      }
    }
  }
};

其实双指针如何保证不重不漏是需要仔细想一想的,这是通过每次i和j的起始位置来保证的。

4Sum Solution:

有了3Sum的基础,4Sum就一样做就行了。

class Solution {
  public:
    vector < vector < int >> res;
  vector < vector < int >> fourSum(vector < int > & nums, int target) {
    sort(nums.begin(), nums.end()); //i每次都是重复序列的第一个
    int i = 0;
    while (i < ((int) nums.size()) - 3) {
      int j = i + 1; //j每次都是从重复序列的第二个开始(如果有的话)
      while (j < ((int) nums.size()) - 2) {
        TwoSum(nums, target, i, j);
        j++;
        while (nums[j] == nums[j - 1] && j < nums.size() - 2) j++;
      }
      i++;
      while (nums[i] == nums[i - 1] && i < nums.size() - 2) i++;
    }
    return res;
  }
  void TwoSum(vector < int > & nums, int target, int i, int j) {
    int k = j + 1, l = nums.size() - 1;
    target = target - nums[i] - nums[j];
    while (k < l) {
      int sum = nums[k] + nums[l];
      if (sum < target) {
        k++;
        while (nums[k] == nums[k - 1] && k < l) k++;
      } else if (sum > target) {
        l--;
        while (nums[l] == nums[l + 1] && k < l) l--;
      } else {
        res.push_back({
          nums[i],
          nums[j],
          nums[k],
          nums[l]
        });
        k++;
        while (nums[k] == nums[k - 1] && k < l) k++;
        l--;
        while (nums[l] == nums[l + 1] && k < l) l--;
      }
    }
  }
};