LeetCode-15-三数之和


题意描述:

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

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


示例:

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

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

解题思路:

Alice: 这个三重循环肯定会超时吧。
Bob: 那肯定的,O(n^2) 都够呛,而且去重应该没那么简单。如果先把所有的答案都求出来,然后再去重可能会不行哦。求解的时候有重复计算,去重的时候还要再附加计算,可能会超时啊。
Alice: emmm, 有数组不能先排个序吗,然后再分析看看,要求 a + b + c == 0那肯定得有正有负吧。
Bob: 不一定哦,万一是 三个零 [0, 0, 0] 呢。
Alice: 那就除了 三个零的情况,至少得有一个正数,至少得有一个负数。
Bob: 我们可以在外层遍历整个排序后的数组, 然后指定最外层遍历的是三元组中的最小值,这样就只遍历 负数 的部分就可以了。
Alice: 对对对,如果三元组中最小值是正数这样的三元组是不成立的,如果最小值是零,刚才已经处理过了。
Bob: 然后我们用双指针来 寻找 三元组中的 中间值和 最大值, 中间值一定要小于等于最大值。
Alice: 也就是说 两个指针 left < right 是一定成立的,然后还有去重的问题,无论是最小值,中间值,最大值的都要去重。
Bob: 对,最小值用 pre 记录上一次访问的值,然后比较呗,如果再遇到就跳过呗。
Alice: 然后就是 leftright, 如果 nums[minIndex] + nums[left] + nums[right] == 0,然后leftright 要向中间移动,一直移动到 两个新的元素为止。
Bob: 如果 nums[minIndex] + nums[left] + nums[right] < 0left 右移,否则 right 左移,应该就可以了。
Alice; 那应该就没错了,最小值,中间值,最大值的去重都考虑了,时间复杂度是O(n^2),空间复杂度应该是O(1),还不错。
Bob: 😎😎

###########################################

Alice: 我们好像想复杂了,不过上面的思路还是在正确的方向上的。
Bob: 你有新想法了 ?
Alice: 对,首先给数组排序,然后循环遍历整个数组,这个最外层的循环遍历的是所有满足条件三元组的最小值,这个最小值应该是无重复的。
Bob: 等一下,如果输入是 [-2,0,1,1,2] 答案中的 [-2,0,2], [-2,1,1] ,两个三元组的最小值不就是重复的吗 ?
Alice: 不是这样理解的,-2 在最外层的循环中应该是无重复的,后面的 [0,2][1,1] 都是在内层循环中找到的。你听我说完先,假设外层循环到了 num[i] 内层循环的任务是找到 数组中 所有 和为-num[i]的二元组,这个可以用双指针去做呀,前面已经给数组排过序了,只需要在 i+1, num.length-1 的范围去找就可以了。然后找到一组后,为了防止有重复的 二元组,两边都要分别跳过相等的元素。这样应该就可以了。
Bob: 好像可以啊,应该还有一写边界条件可以特殊处理一下加速代码。
Alice: 我发现我们之前的代码基本上就是这个意思,就是有点不规范的地方。
Bob: 😲


2023 年7月9日新增解法:排序 + 二分查找

Alice: 还是要多读题呀,题目中说

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0

i j k 是互不相等的,我们把 nums 排序之后,i j k 这些下标之间的关系应该满足 i < j < k,这是满足 i j k 不互相等的条件吧。然后还有返回的三元组不能重复,也就是说 i 和 j 对应的取值不应该有重复的,因为所有的三元组都是 [ i 处的 value,j 处的value,k 处的value ] 的形式,所以 i 和 j 处的 value 不应该有重复,而且只有 i j 的 value 不重复,k 处的 value 也不会重复。
Bob: 说的对,不过二分查找的时候还可以优化。如果 i j 的 value 之和是一个负数,二分查找的开始坐标应该是最小的正数。
Alice: 说的不错,说到正负数,还需要处理一下边界情况。比如说全是正数,全是负数的情况。另外你说的优化也应该考虑到万一没有正数的情况。
Bob: All right


代码:

JavaScript: 排序 + 二分查找

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums) {
    // 升序排列
    const sortNums = nums.sort((a ,b) => a - b);

    // 最小值大于 0
    if (sortNums[0] > 0) {
        return [];
    }
    // 最大值小于 0 
    if (sortNums[sortNums.length - 1] < 0) {
        return [];
    }

    // 在 sortNums 中二分查找
    const binarySearch = (start, end, target) => {
        while(start <= end) {
            let middle = Math.floor((start + end) / 2);
            let val = sortNums[middle];
            if (val === target) {
                return middle;
            }
            if (val > target) {
                end = middle - 1; 
            }
            if (val < target) {
                start = middle + 1;
            }
        }
        return -1; 
    }

    const postiveStartIndex = sortNums.findIndex(item => item > 0);
    const result = [];

    for (let i=0; i<sortNums.length; ++i) {
        // 去重复
        if(i > 0 && sortNums[i-1] === sortNums[i]) {
            continue;
        }
        for(let j=i+1; j<sortNums.length; ++j) {
            // 去重复
            if(j > i+1 && sortNums[j-1] === sortNums[j]) {
                continue;
            }
            const twoSum = sortNums[i] + sortNums[j];
            // 一定找不到
            if (twoSum < 0 && postiveStartIndex === -1) {
                continue;
            }
            let startIndex = j + 1;
            if (twoSum < 0 && postiveStartIndex > startIndex) {
                startIndex = postiveStartIndex;
            }
            const endIndex = sortNums.length - 1;
            const targetIndex = binarySearch(startIndex, endIndex, -1 * twoSum);
            if (targetIndex !== -1) {
                result.push([
                    sortNums[i],
                    sortNums[j],
                    sortNums[targetIndex]
                ]);
            }
        }
    }

    return result;
};

JavaScript 排序 + 双指针,可能是更好理解的版本

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums) {
    // 升序排列
    nums.sort((a, b) => a - b);
    console.log('nums', nums);
    const maxx = nums[nums.length - 1];
    const minn = nums[0];
    // 边界条件
    if (minn > 0 || maxx < 0) {
        return [];
    };

    const res = [];
    for (let i=0; i<=nums.length-3; i++) {
        // 去重
        if (i > 0 && nums[i] === nums[i-1]) {
            continue;
        }
        // “剪枝” 优化
        if (nums[i] > 0) {
            continue;
        }

        let left = i + 1;
        let right = nums.length - 1;
        while (left < right) {
            const tempRes = nums[i] + nums[left] + nums[right];
            if (tempRes === 0) {
                res.push([nums[i], nums[left], nums[right]]);
                left++;
                right--;
                // 去重
                while (left < nums.length && nums[left] === nums[left - 1]) {
                    left++;
                }
                while (right >= 0 && nums[right] === nums[right + 1]) {
                    right--;
                }
            }
            if (tempRes < 0) {
                left++;
            }
            if (tempRes > 0) {
                right--;
            }
        }
        
    }

    return res;
};

JavaScript: 排序 + 双指针

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(num) {

    if(!num || !Array.isArray(num) || num.length === 0){
        return [];
    }
    
    num.sort((a, b) => {return a-b});
    const len = num.length;
    
	// 直接处理边界值输入
    if(num[len-1] < 0){
        return [];
    }
    if(num[0] > 0){
        return [];
    }
    
    let ret = [],
        pre = NaN;
    for(let i=0; i<len; ++i){
        // 最外层循环的是 所有答案中的最小值,最小值应该是无重复的
        if(num[i] === pre){
            continue;
        }
        
        if(num[i] > 0){
            break;
            // num[i]以及后面的数字都是正数
        }else if(num[i] === 0){
            pre = 0;
            // try to find the other zero
            // and there are only one ans for all zeroes
            if(i+2 < len && num[i+2] === 0){
                ret.push([0,0,0]);
            }
        }else{
            pre = num[i];
            // 找出所有可以与 num[i] 相加为 0 的无重复组合
            let tmp = getTarget(num, -num[i], i+1, len-1);
            if(tmp.length){
                tmp.forEach(item => {
                    ret.push([num[i], item[0], item[1]]);
                });
            }
        }
    }
    
    return ret;
};

function getTarget(num, target, i, j){
    
    let ret = []
    while(i < j){ 
        if(num[i] + num[j] > target){
            j--;
        }else if(num[i] + num[j] < target){
            i++;
        }else{
            ret.push([num[i], num[j]]);
            let tmp = num[i];
            // 找到一组之后要“去重”
            while(tmp === num[i]){
                i++;
            }
            tmp = num[j];
            while(tmp === num[j]){
                j--;
            }
        }
    }
    return ret;
}

Python 方法一: 排序 + 双指针。

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:

        ans = []
        if len(nums) < 3:
            return ans
        else:
            nums.sort()
            
            positiveIndex = -1
            zeroCnt       = 0
            for x in range(len(nums)):
                if nums[x] == 0:
                    zeroCnt += 1
                elif nums[x] > 0:
                    positiveIndex = x
                    break

            #print(nums)
            #print(zeroCnt)

            if positiveIndex == -1:
                # 没有正数
                if zeroCnt >= 3:
                    return [[0,0,0]]
                else:
                    return []

            pre = nums[0] + 1
            # pre 初始值不等于 nums[0] 即可
            for x in range(len(nums)):
                # 遍历 所有可能的三元组中最小的那一个
                if nums[x] > 0:
                    # nums[x] > 0, x 后的所有数字都是正数
                    continue
                elif nums[x] < 0:
                    if nums[x] == pre:
                        # 去重, 两个最小值
                        continue
                    else:
                        # 寻找满足条件的三元组
                        pre = nums[x]
                        positiveOne = len(nums)-1
                        # positiveOne 是三元组中最大的 元素的下标
                        anotherOne = x + 1

                        while anotherOne < positiveOne:
                            if nums[x] + nums[anotherOne] + nums[positiveOne] == 0:
                                ans.append([nums[x], nums[anotherOne], nums[positiveOne]])

                                # 去重 
                                while positiveOne > anotherOne and nums[positiveOne] == nums[positiveOne-1]:
                                    positiveOne -= 1
                                while anotherOne < positiveOne and nums[anotherOne] == nums[anotherOne+1]:
                                    anotherOne += 1
                                
                                # 尝试新的答案
                                positiveOne -= 1
                                anotherOne  += 1
                            elif nums[x] + nums[anotherOne] + nums[positiveOne] < 0:
                                anotherOne += 1
                            else:
                                positiveOne -= 1

                else:
                    # nums[x] == 0, 且 0 是三元组中的最小值
                    if zeroCnt >= 3:
                        # 最后一组三元组
                        ans.append([0,0,0])    
                    break

            return ans

Java 方法一: 排序 + 双指针。

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {

        List<List<Integer>> ans = new ArrayList();   // ans 存储答案

        if(nums.length < 3){                         // 不可能有满足条件的三元组,直接返回
            return ans;
        }else{

            Arrays.sort(nums);                        // 排个序
            int zeroCnt = 0;
            int firstPositiveIndex = -1;              // 统计下数组中零的个数 和 第一个正数出现的下标
            for(int i=0; i<nums.length; ++i){
                if(firstPositiveIndex == -1 && nums[i] > 0){
                    firstPositiveIndex = i;
                    break;
                }
                if(nums[i] == 0){
                    zeroCnt += 1;
                }
            }

            if(firstPositiveIndex == -1){               // 一个正数也没有就可以直接返回了
                // nothing, 直接到最后加零走人
            }else{
                int pre = nums[0] + 1;                  // 去重用,pre 初始值和 nums[0] 不同即可
                for(int minIndex = 0; minIndex < nums.length; ++minIndex){   // 以三元组中最小的元素开始搜索
                    if(nums[minIndex] >= 0){
                        break;                            // 后面是没有答案的,可以直接退出循环了
                    }else{
                        if(pre == nums[minIndex]){        // 去重,跳过这个最小元素
                            continue;
                        }
                        pre = nums[minIndex];
                        int left = minIndex + 1;
                        int right = nums.length - 1;       // right 指向三元组最大的一个数,必须是正数
                        while(left < right){
                            if(nums[minIndex] + nums[left] + nums[right] == 0){
                                List<Integer> tmp = new ArrayList();
                                tmp.add(nums[minIndex]);
                                tmp.add(nums[left]);
                                tmp.add(nums[right]);
                                ans.add(tmp);
                                
                                while(right > left && nums[right] == nums[right-1]){
                                    right -= 1;          // 去重,最大值不能重复
                                }
                                while(left < right && nums[left] == nums[left+1]){
                                    left += 1;           // 去重,中间值不能重复
                                }
                                left  += 1;              // 一直到 left 和 right 指向下两个不一样的值 
                                right -= 1; 
                                
                            }else if(nums[minIndex] + nums[left] + nums[right] < 0){
                                left += 1;
                            }else{
                                right -= 1;
                            }
                        }
                    }
                }
            }
            if(zeroCnt >= 3){
                List<Integer> zeroAns = new ArrayList();   // 准备三个零的答案
                zeroAns.add(0);
                zeroAns.add(0);
                zeroAns.add(0);
                ans.add(zeroAns);
            }
            return ans;
        }
    }
}

易错点:

  • 一些测试用例:
[-1,0,1,2,-1,-4]
[-1,-2,-3,0,0,0]
[-4,2,2,2,1,3,0,0,0,4]
[-2,-2,-4,0,0,0,1,1,2,2,4]
[0,2,3,3]
[-1,-2]
[2,3,4,5]
  • 答案:
[[-1,-1,2],[-1,0,1]]
[[0,0,0]]
[[-4,0,4],[-4,1,3],[-4,2,2],[0,0,0]]
[[-4,0,4],[-4,2,2],[-2,-2,4],[-2,0,2],[-2,1,1],[0,0,0]]
[]
[]
[]

总结:


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值