[data structure and algorithm] in-depth analysis of the solution idea and algorithm example of "sum of three numbers"

Posted by bingo333 on Fri, 14 Jan 2022 22:33:25 +0100

1, Title Requirements

  • Give you an array num containing n integers. Judge whether there are three elements a, b and c in num, so that a + b + c = 0? Please find all triples with sum 0 and no repetition.
  • Note: the answer cannot contain duplicate triples.
  • Example 1:
Input: nums = [-1,0,1,2,-1,-4]
Output:[[-1,-1,2],[-1,0,1]]
  • Example 2:
Input: nums = []
Output:[]
  • Example 3:
Input: nums = [0]
Output:[]
  • Tips:
    • 0 <= nums.length <= 3000;
    • -105 <= nums[i] <= 105.

2, Solving algorithm

① Quick sort

  • Sort the array first;
  • Starting from the left, select a value as the fixed value, and solve it on the right to obtain two values added to 0;
  • Similar to fast platoon, define the head and tail;
  • Sum of head and tail and constant value:
    • Equal to 0, record these three values;
    • Less than 0, the head moves to the right;
    • Greater than 0, the tail moves left;
  • Move the fixed value to the right and repeat this step.

  • Java example:
var threeSum = function(nums) {
  // The leftmost value is the fixed value, and all values on the right are calculated by pushing on both sides
  let res = [];
  nums.sort((a, b) => a - b);
  let size = nums.length;
  if (nums[0] <= 0 && nums[size - 1] >= 0) {
    // Ensure that there are positive and negative numbers
    let i = 0;
    while (i < size - 2) {
      if (nums[i] > 0) break; // Leftmost greater than 0, no solution
      let first = i + 1;
      let last = size - 1;
      while (first < last) {
        if (nums[i] * nums[last] > 0) break; // Three numbers with the same sign, no solution
        let sum = nums[i] + nums[first] + nums[last];
        if (sum === 0) {
          res.push([nums[i], nums[first], nums[last]]);
        }
        if (sum <= 0) {
          // Negative number is too small, first shift right
          while (nums[first] === nums[++first]) {} // Duplicate value skip
        } else {
          while (nums[last] === nums[--last]) {} // Duplicate value skip
        }
      }
      while (nums[i] === nums[++i]) {}
    }
  }

  return res;
};

② Sort + double pointer

  • The title requires to find all triples with "no repetition" and sum of 0. This "no repetition" requirement makes it impossible to simply enumerate all triples with a triple loop. This is because in the worst case, all the elements in the array are 0, that is:
[0, 0, 0, 0, 0, ..., 0, 0, 0]
  • The sum of any triplet is 0. If you directly use the triple loop to enumerate triples, you will get O(N3) triples that meet the requirements of the topic (where n is the length of the array). The time complexity is at least O(N3). After that, the hash table needs to be used for de duplication operation to get the final answer without duplicate triples, which consumes a lot of space. The time complexity and space complexity of this method are very high, so we should think about this problem in a different way.
  • What is the essence of "non repetition"? We keep the big framework of the triple cycle unchanged and only need to ensure that:
    • The elements enumerated in the second loop are not less than the elements enumerated in the current first loop;
    • The elements enumerated by the third loop are not less than the elements enumerated by the current second loop.
  • In other words, the enumerated triples (a,b,c) satisfy a ≤ b ≤ C, which ensures that only the order of (a,b,c) will be enumerated, while (b,a,c), (c,b,a) will not, so as to reduce repetition. To achieve this, you can sort the elements in the array from small to large, and then use an ordinary triple loop to meet the above requirements.
  • At the same time, for each loop, the elements of two adjacent enumerations cannot be the same, otherwise it will cause repetition. For example, if the sorted array is:
[0, 1, 2, 2, 2, 3]
 ^  ^  ^
  • The first triplet enumerated by using the triple loop is (0,1,2). If the third loop continues to enumerate the next element, it is still a triplet (0,1,2), resulting in duplication. Therefore, you need to "jump" the third loop to the next different element, that is, the last element 3 in the array, and enumerate the triples (0,1,3).
  • The pseudo code implementation of the improved method is given below:
nums.sort()
for first = 0 .. n-1
    // Enumeration is performed only if the element is different from the last enumeration
    if first == 0 or nums[first] != nums[first-1] then
        for second = first+1 .. n-1
            if second == first+1 or nums[second] != nums[second-1] then
                for third = second+1 .. n-1
                    if third == second+1 or nums[third] != nums[third-1] then
                        // Judge whether there is a+b+c==0
                        check(first, second, third)
  • The time complexity of this method is still O(N3). After all, it still doesn't jump out of the big framework of triple cycle. However, it is easy to continue the optimization. It can be found that if we fix the elements a and B enumerated by the first two loops, then only the unique c satisfies a+b+c=0. When the second loop enumerates an element B ', since B' > b, c 'satisfying a + b' + c '= 0 must have c' < c, that is, c 'must appear on the left side of c in the array. In other words, B can be enumerated from small to large, and c can be enumerated from large to small, that is, the second loop and the third loop are actually parallel.
  • With this discovery, we can keep the second loop unchanged and turn the third loop into a pointer moving to the left from the right end of the array, so as to obtain the following pseudo code:
nums.sort()
for first = 0 .. n-1
    if first == 0 or nums[first] != nums[first-1] then
        // Pointer corresponding to the third cycle
        third = n-1
        for second = first+1 .. n-1
            if second == first+1 or nums[second] != nums[second-1] then
                // Move the pointer to the left until a+b+c is not greater than 0
                while nums[first]+nums[second]+nums[third] > 0
                    third = third-1
                // Judge whether there is a+b+c==0
                check(first, second, third)
  • This method is often called "double pointer". When we need to enumerate two elements in the array, if we find that the second element decreases with the increase of the first element, we can use the double pointer method to reduce the time complexity of enumeration from O(N2) to O(N). Why O(N)? This is because in each step of the enumeration process, the "left pointer" will move one position to the right (that is, b in the title), and the "right pointer" will move several positions to the left. This is related to the elements of the array, but we know that the total number of positions it will move is O(N), which is evenly spread, and it will also move one position to the left each time. Therefore, the time complexity is O(N).
  • Note that there is also the first loop in our pseudo code, and the time complexity is O(N), so the total time complexity of enumeration is O(N2). Since the time complexity of sorting is O(N logN), which is less than the former in the progressive sense, the total time complexity of the algorithm is O(N2).
  • C + + example:
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        vector<vector<int>> ans;
        // Enumer a tiona
        for (int first = 0; first < n; ++first) {
            // The number required is different from that of the last enumeration
            if (first > 0 && nums[first] == nums[first - 1]) {
                continue;
            }
            // The pointer corresponding to c initially points to the rightmost end of the array
            int third = n - 1;
            int target = -nums[first];
            // Enum b
            for (int second = first + 1; second < n; ++second) {
                // The number required is different from that of the last enumeration
                if (second > first + 1 && nums[second] == nums[second - 1]) {
                    continue;
                }
                // You need to make sure that the pointer of b is to the left of the pointer of c
                while (second < third && nums[second] + nums[third] > target) {
                    --third;
                }
                // If the pointers coincide, with the subsequent increase of b
                // There will be no c satisfying a+b+c=0 and B < c. you can exit the cycle
                if (second == third) {
                    break;
                }
                if (nums[second] + nums[third] == target) {
                    ans.push_back({nums[first], nums[second], nums[third]});
                }
            }
        }
        return ans;
    }
};
  • Java example:
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        // Enumer a tiona
        for (int first = 0; first < n; ++first) {
            // The number required is different from that of the previous enumeration
            if (first > 0 && nums[first] == nums[first - 1]) {
                continue;
            }
            // The pointer corresponding to c initially points to the rightmost end of the array
            int third = n - 1;
            int target = -nums[first];
            // Enum b
            for (int second = first + 1; second < n; ++second) {
                // The number required is different from that of the last enumeration
                if (second > first + 1 && nums[second] == nums[second - 1]) {
                    continue;
                }
                // You need to make sure that the pointer of b is to the left of the pointer of c
                while (second < third && nums[second] + nums[third] > target) {
                    --third;
                }
                // If the pointers coincide, with the subsequent increase of b
                // There will be no c satisfying a+b+c=0 and B < c. you can exit the cycle
                if (second == third) {
                    break;
                }
                if (nums[second] + nums[third] == target) {
                    List<Integer> list = new ArrayList<Integer>();
                    list.add(nums[first]);
                    list.add(nums[second]);
                    list.add(nums[third]);
                    ans.add(list);
                }
            }
        }
        return ans;
    }
}
  • Complexity analysis:
    • Time complexity: O(N2), where n is the length of array nums.
    • Spatial complexity: O(logN). Ignoring the space for storing answers, the space complexity of additional sorting is O(logN). However, the input array num is modified, which is not necessarily allowed in practice. Therefore, it can also be regarded as using an additional array to store copies of num and sort them. The space complexity is O(N).

Topics: data structure leetcode quick sort