三数之和

79 阅读2分钟

题目

回溯

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) {

        Main main = new Main();
        int [] height = new int[] {-1, 0, 1, 2, -1, 4};
        main.threeSum(height);
    }

    int [] nums;
    List<List<Integer>> ans = new ArrayList<>();
    Map<Integer, Integer> numUseMap = new HashMap<>();
    public List<List<Integer>> threeSum(int[] nums) {
        this.nums = nums;
        for (int i = 0; i < nums.length; i ++) {
            numUseMap.put(nums[i], numUseMap.getOrDefault(nums[i], 0) + 1);
        }
        huisu(new ArrayList<>(), 0);
        return ans;
    }

    public void huisu(List<Integer> path, int tartget) {
        // 终止条件
        if (path.size() > 3) {
            return;
        }
        if (path.size() == 3) {
            if (tartget == 0) {
                ans.add(new ArrayList<>(path));
            }
            return;
        }
        for (int i = 0; i < nums.length; i ++) {
            int count = numUseMap.get(nums[i]);
            if (count > 0) {
                // 进行选择
                numUseMap.put(nums[i], count - 1);
                path.add(nums[i]);
                huisu(path, tartget + nums[i]);
                // 撤销选择
                path.remove(path.size() - 1);
                numUseMap.put(nums[i], count);
            }
            }
        }

}

基本思路

  1. 通过hashmap记录每个元素可使用的次数, 利用回溯法进行全遍历

缺点

  1. 最后的结果出现大量的元素相同的list, 去重的成本非常大, 因此需要考虑如何在遍历的过程中完成去重

三重循环

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {

        Main main = new Main();
        int [] height = new int[] {0, 0, 0, 0};
        main.threeSum(height);
    }


    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> ans = new ArrayList<>();
        if (nums.length < 3) {
            return ans;
        }
        // 首先对数组排序
        Arrays.sort(nums);

        // 利用三个指针 i left right
        // 寻找-nums[i] = nums[left] + nums[right]的情况
        Integer preI = null;
        for (int i = 0; i < nums.length; i ++) {
            if (nums[i] > 0) {
                // 如果nums[i] > 0, 那么left和right对应的数字肯定>0
                break;
            }
            if (preI == null) {
                preI = nums[i];
            } else if (nums[i] == preI) {
                // 如果当前数字和之前的数字相同, 跳过当前数字
                continue;
            }
            int left = i + 1;
            Integer preLeft = null;
            while (left < nums.length) {
                if (preLeft == null) {
                    preLeft = nums[left];
                } else if (nums[left] == preLeft) {
                    // 如果当前数字和之前的数字相同, 跳过当前数字
                    left++;
                    continue;
                }
                if (nums[i] > 0 && nums[left] > 0) {
                    left ++;
                    break;
                }
                int right = left + 1;
                Integer preRight = null;
                while (right < nums.length) {
                    if (preRight == null) {
                        preRight = nums[right];
                    } else if (nums[right] == preRight) {
                        // 如果当前数字和之前的数字相同, 跳过当前数字
                        right ++;
                        continue;
                    }
                    if (nums[i] + nums[left] > 0 && nums[right] > 0) {
                        right ++;
                        break;
                    }
                    if (nums[i] + nums[left] + nums[right] == 0) {
                        List<Integer> temp = new ArrayList<>();
                        temp.add(nums[i]);
                        temp.add(nums[left]);
                        temp.add(nums[right]);
                        ans.add(temp);
                    }
                    preRight = nums[right];
                    right ++;
                }
                preLeft = nums[left];
                left ++;

            }
            preI = nums[i];
        }
        return ans;

    }
}

基本思路

  1. 利用i, left, right表示数组中的三个元素, 通过对数组排序, 以及记录i, left, right上一个元素值的方式, 每个位置如果遇见重复的元素, 就跳过. 通过有序数组 + 相同元素跳过的方式解决了答案重复性的问题

  2. 因为是有序的, 所以比较每个位置当前值和目标值是可以对遍历截断的, 减少计算次数

缺点

  1. 本质依旧是三重遍历, 最优几个用例会超时

循环 + 双指针

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {

        Main main = new Main();
        int [] height = new int[] {-1, 0, 1, 2, -1, 4};
        main.threeSum(height);
    }


    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> ans = new ArrayList<>();
        if (nums.length < 3) {
            return ans;
        }
        // 首先对数组排序
        Arrays.sort(nums);

        // 利用三个指针 i left right
        // 寻找nums[i] + nums[left] + nums[right] == 0的情况
        for (int i = 0; i < nums.length; i ++) {
            if (nums[i] > 0) {
                // 如果nums[i] > 0, 那么left和right对应的数字肯定>0
                break;
            }
            if ((i - 1) >= 0 && nums[i] == nums[i - 1]) {
                // 遇见重复元素跳过
                continue;
            }
            // 左右指针互相配合
            int left = i + 1;
            int rightLimit = nums.length - 1;
            while (left < nums.length) {
                if ((left - i) > 1 && nums[left] == nums[left - 1]) {
                    // 遇见重复元素跳过
                    left ++;
                    continue;
                }
                if (nums[i] + nums[left] > 0) {
                    break;
                }
                // 右指针从数组右侧向左,
                int right = rightLimit;
                while (right > left) {
                    if ((right + 1) < nums.length && nums[right] == nums[right + 1]) {
                        // 遇见重复元素跳过
                        right --;
                        continue;
                    }
                    if (nums[i] + nums[left] + nums[right] < 0) {
                        break;
                    }
                    if (nums[i] + nums[left] + nums[right] == 0) {
                        // 因为下一次当left增加时, 可能的right只会出现在当前right的左边,相当于缩小了下一次的范围
                        rightLimit = right;
                        List<Integer> temp = new ArrayList<>();
                        temp.add(nums[i]);
                        temp.add(nums[left]);
                        temp.add(nums[right]);
                        ans.add(temp);
                    }
                    right --;
                }
                left ++;
            }
        }
        return ans;
    }

}

基本思路

  1. 发现left指针的变化可能会缩短right指针的变化范围, 因此将该因素考虑. 例如当a1 + b1 + c1 = 0 成立, 此时b1增加成b2, c2可能的范围一定是小于c1的

  2. 注意每重循环里的条件, 及时对循环进行终止, 光用==0不行, 必须有如下的条件:

                    if (nums[i] + nums[left] > 0) {
                        break;
                    }
                    if (nums[i] + nums[left] + nums[right] < 0) {
                        break;
                    }