LeetCode15 三数之和 3Sum & 18 四数之和 4Sum 解法总结

3Sum 题目

在这里插入图片描述
要点:

  • 可能包含多组解
  • 解答不能重复,比如:[-1, 0, 1] 和 [0, -1, 1] 视为重复解

3Sum 解题

暴力遍历

没有思路的时候,可以先使用暴力遍历的方法解题,然后再思考怎么优化。平时做项目的时候也是如此,没有好思路的时候,至少先找到一个可行方案,有了备选方案后,再继续思考更优的解法。

使用暴力遍历的话,逻辑上是很简单的。

再有一个是不能有重复解,采用的方法:每次找到一组解,对这组解进行排序,并判断这组解是否已经存在,不存在的话再加进要返回的list里。

固定一个数 + 两数之和哈希解法

结合【两数之和】的解法:固定第一个数 x,接下来就是在剩余的数里找 y + z = -x,从而转化成两数之和问题。

# python
class Solution:
	def threeSum(self, nums: List[int]) -> List[List[int]]:
		ret = []
		if len(nums) < 3:
			return ret
		for i in range(len(nums) - 2):
			sum2 = -nums[i]
			nums_dict = {}
			for j in range(i + 1, len(nums)):
				if sum2 - nums[j] in nums_dict.keys():
					temp = [nums[i], nums[j], sum2 - nums[j]]
					if sorted(temp) not in ret:
						ret.append(sorted(temp))
				else:
					nums_dict[nums[j]] = j
		return ret

一个简单的优化:与其每次找到解的时候排序,不如一次性在最开始对整个数组进行排序。

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        ret = []
        if len(nums) < 3:
            return ret
        nums.sort() # O(nlogn)
        for i in range(len(nums) - 2): # O(n^2)
            sum2 = -nums[i]
            nums_dict = {}
            for j in range(i + 1, len(nums)):
                if nums[j] in nums_dict.keys():
                    temp = [nums[i], sum2 - nums[j], nums[j]]
                    if temp not in ret:
                        ret.append(temp)
                else:
                    nums_dict[sum2 - nums[j]] = j
        return ret

排序 + 双指针

官方解答非常详细,这里截取一些方法论,以后遇到类似的情况要能够联想到并应用起来!

  • 为什么想到排序?
    在这里插入图片描述
  • 为什么想到用双指针?List item
    当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法。

官方的解法:

  • [左指针] 每次右移一个位置,[右指针] 每次左移若干个位置
  • 第一重循环枚举的 first element 与上一次不同,第二重循环枚举的 second element 与上一次不同:不会出现重复解
// javascript
var threeSum = function(nums) {
    const n = nums.length;
    const res = [];
    if (n < 3) return res;
    nums.sort((a, b) => a - b);                                  // 升序排列
    for (let i = 0; i < n - 2; i++) {                            // 枚举 a
    	if (nums[i] > 0) return res;
        if (i > 0 && nums[i] === nums[i - 1]) continue;          // 需要和上次枚举的数不同
        const target = -nums[i];
        let k = n - 1;                                           // c 对应的指针初始指向数组的最右端
        for (let j = i + 1; j < n - 1; j++) {                    // 枚举 b
            if (j > i + 1 && nums[j] === nums[j - 1]) continue;  // 需要和上次枚举的数不同
            while (j < k && nums[j] + nums[k] > target) {        // 需要保证 b 的指针在 c 的指针的左侧
                k--;
            }
            // 如果指针重合,随着 b 后续的增加
            // 就不会有满足 a + b + c = 0 并且 b < c 的 c 了,可以退出循环
            if (j === k) break;
            if (nums[j] + nums[k] === target) res.push([nums[i], nums[j], nums[k]]);
        }
    }
    return res;
};

换一种写法:

// javascript
var threeSum = function(nums) {
    const n = nums.length;
    const res = [];
    if (n < 3) return res;
    nums.sort((a, b) => a - b);
    for (let i = 0; i < n - 2; i++) {
        if (i > 0 && nums[i] === nums[i - 1]) continue;    // 不重复枚举 a
        const target = -nums[i];
        let j = i + 1, k = n - 1;                          // 双指针
        while (j < k) {
            if ((j > i + 1 && nums[j] === nums[j - 1]) || nums[j] + nums[k] < target) {
                j++;                                       // 不重复枚举 b
            } else if ((k < n - 1 && nums[k] === nums[k + 1]) || nums[j] + nums[k] > target) {
                k--;                                       // 不重复枚举 c
            } else {
                res.push([nums[i], nums[j], nums[k]]);
                j++;
                k--;
            }
        }
    }
    return res;
};

可以加两个剪枝条件:

// javascript
var threeSum = function(nums) {
    const n = nums.length;
    const res = [];
    if (n < 3) return res;
    nums.sort((a, b) => a - b);
    for (let i = 0; i < n - 2; i++) {
        // 固定 i 后能取到的最小三数之和 > 0, 而下一次循环 a 会变得更大 => 提前知道后续不可能找到满足条件的三个数
        // 因为这题比较特殊, 和取的是 0, 它是一个非负数, 提前结束的条件也可以写成:if (nums[i] > 0) break;
    	if (nums[i] + nums[i + 1] + nums[i + 2] > 0) break;
        // 不重复枚举 a
        // 固定 i 后能取到的最大三数之和 < 0 => 在当前 i 下, 不可能有 j,k 能满足条件, 直接枚举下一个 a 
        if (i > 0 && nums[i] === nums[i - 1] || nums[i] + nums[n - 1] + nums[n - 2] < 0) continue;
        const target = -nums[i];
        let j = i + 1, k = n - 1;                          // 双指针
        while (j < k) {
            if ((j > i + 1 && nums[j] === nums[j - 1]) || nums[j] + nums[k] < target) {
                j++;                                       // 不重复枚举 b
            } else if ((k < n - 1 && nums[k] === nums[k + 1]) || nums[j] + nums[k] > target) {
                k--;                                       // 不重复枚举 c
            } else {
                res.push([nums[i], nums[j], nums[k]]);
                j++;
                k--;
            }
        }
    }
    return res;
};

时间复杂度 O ( N 2 ) O(N^2) O(N2) -> 排序 O ( N l o g N ) O(NlogN) O(NlogN),遍历两层循环 O ( N 2 ) O(N^2) O(N2)
空间复杂度 O ( l o g N ) O(logN) O(logN) -> 我们忽略存储答案的空间,额外的排序的空间复杂度为 O ( l o g N ) O(log N) O(logN)。然而我们修改了输入的数组nums,在实际情况下不一定允许,因此也可以看成使用了一个额外的数组存储了nums 的副本并进行排序,空间复杂度为 O ( N ) O(N) O(N)

4Sum 题目及解题

在这里插入图片描述
其实是一道换汤不换药的【三数之和】题:固定第一个数 a,寻找 b + c + d = target - a -> 三数之和问题,【四数之和】和【三数之和】一样有多组解,且解不能重复。在【三数之和】问题中,我们已经分析过如何去重复,就不赘述了。

// javascript
var fourSum = function(nums, target) {
    const n = nums.length;
    const res = [];
    if (n < 4) return res;
    nums.sort((a, b) => a - b);
    for (let a = 0; a < n - 3; a++) {                            // 固定第一个数
        if (a > 0 && nums[a] === nums[a - 1]) continue;          // 不能重复
        for (let b = a + 1; b < n - 2; b++) {                    // 固定第二个数
            if (b > a + 1 && nums[b] === nums[b - 1]) continue;  // 不能重复
            const sumTwo = target - nums[a] - nums[b];
            let c = b + 1, d = n - 1;
            // 双指针遍历
            while (c < d) {
                if ((c > b + 1 && nums[c] === nums[c - 1]) || nums[c] + nums[d] < sumTwo) {
                    c++;
                } else if ((d < n - 1 && nums[d] === nums[d + 1]) || nums[c] + nums[d] > sumTwo) {
                    d--;
                } else {
                    res.push([nums[a], nums[b], nums[c], nums[d]]);
                    c++;  // !!!
                    d--;  // !!!
                }
            }
        }
    }
    return res;
};

来进行一个小优化,加一些限制条件:在确知无解的情况下,提前结束当前这层循环或进行该循环的下一个遍历值:
在这里插入图片描述

// javascript
var fourSum = function(nums, target) {
    const n = nums.length;
    const res = [];
    if (n < 4) return res;
    nums.sort((a, b) => a - b);
    for (let a = 0; a < n - 3; a++) {
        if (nums[a] + nums[a + 1] + nums[a + 2] + nums[a + 3] > target) break;
        if ((a > 0 && nums[a] === nums[a - 1]) || nums[a] + nums[n - 1] + nums[n - 2] + nums[n - 3] < target) continue;
        for (let b = a + 1; b < n - 2; b++) {
            if (nums[a] + nums[b] + nums[b + 1] + nums[b + 2] > target) break;
            if ((b > a + 1 && nums[b] === nums[b - 1]) || nums[a] + nums[b] + nums[n - 1] + nums[n - 2] < target) continue;
            const sumTwo = target - nums[a] - nums[b];
            let c = b + 1, d = n - 1;
            while (c < d) {
                if ( (c > b + 1 && nums[c] === nums[c - 1]) || nums[c] + nums[d] < sumTwo) {
                    c++;
                } else if ((d < n - 1 && nums[d] === nums[d + 1]) || nums[c] + nums[d] > sumTwo) {
                    d--;
                } else {
                    res.push([nums[a], nums[b], nums[c], nums[d]]);
                    c++;
                    d--;
                }
            }
        }
    }
    return res;
};

时间复杂度: O ( n 3 ) O(n^3) O(n3),其中 n n n 是数组的长度。排序的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn),枚举四元组的时间复杂度是 O ( n 3 ) O(n^3) O(n3),因此总时间复杂度为 O ( n 3 + n l o g n ) = O ( n 3 ) O(n^3+nlogn)=O(n^3) O(n3+nlogn)=O(n3)

空间复杂度: O ( l o g n ) O(logn) O(logn),其中 n n n 是数组的长度。空间复杂度主要取决于排序额外使用的空间。此外排序修改了输入数组 nums,实际情况中不一定允许,因此也可以看成使用了一个额外的数组存储了数组 nums 的副本并排序,空间复杂度为 O ( n ) O(n) O(n)

3Sum官方解题
算法思维养成记【双指针和排序去重】- 三数之和
4Sum官方解题

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值