这道题可以说是昨天Leetcode11 2Sum的变体,本质上的思想还是用到了two pointers,但是需要注意的是,这道题的特殊情况是不能有重复,所以我们需要加一下限制条件。

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        res=[]
        nums.sort()
        for i in range(len(nums)):
            if nums[i] > 0:
                break
            if i == 0 or nums[i - 1] != nums[i]:
                self.twosum(nums,i,res)
        return res
    def twosum(self,nums:List[int],i: int,res:List[List[int]]):
        l , r = i + 1,len(nums) - 1
        while l < r :
            sum = nums[i] + nums[l] + nums[r]
            if sum < 0 :
                l += 1
            elif sum > 0 :
                r -= 1
            else:
                res.append([nums[i],nums[l],nums[r]])
                l += 1
                r -= 1
                while l < r and nums[l] == nums[l - 1]:
                    l += 1

在这个思路中,我们先进行排序,排序的目的是便于使用双指针技术,同时也可以在过程中快速跳过重复的元素,避免重复解。

然后我们使用 for 循环依次固定一个数 nums[i] 作为第一个数。如果 nums[i] > 0,可以直接结束循环,因为排序后数组的元素逐渐增大,无法再找到满足 a + b + c = 0 的组合(正数相加不会等于 0)。跳过重复值:if i == 0 or nums[i - 1] != nums[i] 用于确保不会重复使用相同的起点数。

然后我们调用twosum函数寻找之后的两个数,sum小于0的情况说明l需要右移,大于0则说明r需要左移,同样的我们在这里也要判断是否有重复的元素。

在做完这道题以后,如果我们需要一个4Sum又应该怎么操作呢

在上面的3Sum 我们看到 实际上可以把问题转化为2Sum来解决,那么针对4Sum,我们可以先变成3Sum,再变成2Sum,这就是递归的思想了,所以很快 ,我就有了这样一个思路。

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()
        results = []
        self.findsum(nums,target,4,[],results)
        return results

    def findsum(self,nums:List[int],target: int,N:int,res:List[int],results:List[List[int]]):
        if len(nums) < 2 or N < 2:
            return
        if N == 2:
            l,r = 0, len(nums) -1
            while l < r :
                if nums[l] + nums[r] == target :
                    results.append(res + [nums[l] , nums[r]])
                    l += 1
                    r -= 1
                    while l < r and nums[l] == nums[l-1]:
                        l += 1
                elif nums[l] + nums[r] < target:
                    l += 1
                else:
                    r -= 1
        else:
            for i in range(0,len(nums)-N+1):
                if target < nums[i] * N or target > nums[-1] * N:
                    break
                if i == 0 or i > 0 and nums[i-1] != nums[i]:
                    self.findsum(nums[i+1:],target-nums[i],N - 1,res + [nums[i]],results)
        return 

那么我们首先要做的就是排序,然后建立一个可以递归的函数,N == 2 是我们的basecase,然后就是一些边界条件的考量。在写代码的过程中我还发现了一个很有趣的点,那就是r pointer的去重,我在写的过程中发现这行代码实际是不需要的

while l < r and nums[r] == nums[r+1]:
    r -= 1

那么就有一个很有意思的点就是为什么l的去重是必须的,而r不是

l的去重逻辑:

while l < r and nums[l] == nums[l - 1]:
    l += 1

1.l必须跳过重复值

  • 作用: 在当前解法中,l向右移动是为了寻找新的数字组合。如果l没有跳过重复的值,那么在下一次计算中,会重新处理已经生成的组合,导致结果重复。

2. l从 l = i+1 开始,与右指针的范围独立

  • l是 N > 2 时递归向下传递的主要变量,因此对l的去重更直接影响最终的结果。

r去重的必要性较低

r的去重逻辑

while l < r and nums[r] == nums[r + 1]:
    r -= 1

1. 自然的r移动已避免重复

  • 在two pointers中,找到符合条件的组合后,r会被直接左移(r -= 1),这通常已经足够避免r重复值被多次处理。
  • r的重复值对结果的影响较低,因为大多数情况下它仅影响一次组合生成后的局部指针位置,而不影响最终结果集。

2. 如果保留l去重,r可以省略

  • 如果l已经跳过了重复值,并且我们总是先处理l,再移动r,重复值问题已经通过l的去重逻辑解决。
  • r的去重代码可以作为冗余逻辑移除。

两者有一个是否足够?

保留l,省略r:推荐

  • l跳过重复值是必须的,因为它是寻找新组合的核心逻辑。
  • 如果r没有去重,依靠右指针的自然移动(r -= 1),也不会影响结果正确性。

省略l,保留r:可能出问题

  • 如果l没有跳过重复值,那么从头到尾处理相同l值时会多次生成相同的组合。
  • 即使r跳过了重复值,仍然会导致最终结果中存在重复。



举例分析

输入:

nums = [-2, -1, -1, 0, 1, 2]target = 0,固定 nums[i] = -2

l去重保留:

  • 初始左指针:l = 1nums[l] = -1
  • 右指针:r = 5nums[r] = 2
  • 找到组合 [-2, -1, 1] 后:
    • 左指针跳过重复值,移动到 nums[l+1] = 0
    • 确保下一次计算从不同值开始。

如果省略l去重:

  • 初始左指针:l = 1nums[l] = -1
  • 右指针:r = 5nums[r] = 2
  • 找到组合 [-2, -1, 1] 后:
    • 左指针不跳过重复值,会再次处理相同的 nums[l] = -1,导致重复结果。
Jimmy仔 头像

Published by

Categories:

留下评论