nSum 问题通用模板详解
如果你刷过 LeetCode 的 Two Sum、3Sum、4Sum,很容易发现它们其实在问同一类问题:
从数组里选出
n个数,让它们的和等于target,并且结果不能重复。
这类题可以统一叫做 nSum 或 KSum 问题。一个非常经典的通用模板就是:先排序,然后把 nSum 递归降维成 (n-1)Sum,一直降到 2Sum,最后用双指针解决。
一句话概括这个模板:
nSum的核心不是一次性找出 n 个数,而是先固定一个数,再去剩下的数组里找(n-1)Sum。
一、问题从 3Sum 开始
以 3Sum 为例,题目通常是:
给定一个整数数组 nums,找出所有不重复的三元组 [a, b, c],使得:
a + b + c = 0
代码为:
from typing import List
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
return self.nSumTarget(nums, 3, 0, 0)
它只做两件事:
- 先排序。
- 调用通用的
nSumTarget(nums, 3, 0, 0)。
这里的参数含义是:
nSumTarget(nums, n, start, target)
也就是:从 nums[start:] 这个范围里,找出所有由 n 个数组成、和为 target 的组合。
所以 threeSum(nums) 本质上就是从整个数组里,找 3 个数,使它们的和为 0。
二、为什么一定要先排序?
排序是这个模板成立的前提。它主要有两个作用。
第一个作用是让 2Sum 可以使用双指针。
数组有序后,左指针 left 指向较小的数,右指针 right 指向较大的数:
nums[left] + nums[right] < target -> left 右移,让和变大
nums[left] + nums[right] > target -> right 左移,让和变小
nums[left] + nums[right] == target -> 找到一组答案
第二个作用是方便去重。
比如排序后数组长这样:
[-4, -1, -1, 0, 1, 2]
相同元素会挨在一起。这样在遍历或移动指针时,只要发现当前元素和上一个元素相同,就可以跳过,避免产生重复组合。
三、递归思想:把 nSum 变成 2Sum
模板最关键的地方在这里:
sub = self.nSumTarget(nums, n - 1, i + 1, target - nums[i])
当我们要求 n 个数的和为 target 时,可以先固定一个数 nums[i]。
那么剩下的问题就变成:
从 i + 1 之后的数组里,找 n - 1 个数,使它们的和等于 target - nums[i]
也就是:
nSum(nums, n, start, target) = 固定 nums[i] + (n-1)Sum(nums, n-1, i+1, target-nums[i])
拿 3Sum 举例:
找 3 个数,和为 0
可以先固定一个数,比如 -1,那么问题就变成:
找 2 个数,和为 1
这就是递归降维。
如果是 4Sum,过程也是一样:
4Sum -> 固定一个数 -> 3Sum
3Sum -> 固定一个数 -> 2Sum
2Sum -> 双指针解决
所以这套方法可以自然扩展到任意固定的 n。
四、为什么最终停在 2Sum?
递归一定要有终点。在这个模板里,终点就是 2Sum:
if n == 2:
left, right = start, sz - 1
while left < right:
sum_val = nums[left] + nums[right]
left_val, right_val = nums[left], nums[right]
if sum_val < target:
while left < right and nums[left] == left_val:
left += 1
elif sum_val > target:
while left < right and nums[right] == right_val:
right -= 1
else:
res.append([left_val, right_val])
while left < right and nums[left] == left_val:
left += 1
while left < right and nums[right] == right_val:
right -= 1
为什么停在
2Sum?
因为有序数组里的2Sum可以在线性时间内解决,复杂度是O(N)。
如果继续递归到1Sum,反而没有必要。2Sum的双指针已经是这套模板里最高效、最稳定的终点。
五、去重逻辑是这道题的灵魂
nSum 难的不是找到答案,而是避免重复答案。
这份代码里有两层去重。
1. 递归层去重
当 n > 2 时,会枚举一个固定数:
for i in range(start, sz):
if i > start and nums[i] == nums[i - 1]:
continue
这段代码的意思是:在同一层递归里,如果当前固定的数和前一个固定的数相同,就跳过。
注意这里的判断是:
i > start
而不是简单写成:
i > 0
因为每一层递归都有自己的起点 start。我们只需要跳过同一层中的重复选择,不能误伤不同递归层里的合法选择。
2. 2Sum 层去重
在 2Sum 中,如果找到了一个答案:
res.append([left, right])
还要继续跳过所有重复的 left 和 right:
while left < right and nums[left] == left_val:
left += 1
while left < right and nums[right] == right_val:
right -= 1
如果当前和太小,也不是简单 left += 1,而是跳过所有相同的 left_val:
while left < right and nums[left] == left_val:
left += 1
如果当前和太大,也跳过所有相同的 right_val:
while left < right and nums[right] == right_val:
right -= 1
这样可以保证结果集中不会出现重复组合。
六、完整模板代码
from typing import List
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
return self.nSumTarget(nums, 3, 0, 0)
# 注意:调用这个函数之前一定要先给 nums 排序
# n:要求几个数之和
# start:从哪个索引开始找
# target:目标和
def nSumTarget(self, nums: List[int], n: int, start: int, target: int) -> List[List[int]]:
sz = len(nums)
res = []
if n < 2 or sz < n:
return res
if n == 2:
left, right = start, sz - 1
while left < right:
sum_val = nums[left] + nums[right]
left_val, right_val = nums[left], nums[right]
if sum_val < target:
while left < right and nums[left] == left_val:
left += 1
elif sum_val > target:
while left < right and nums[right] == right_val:
right -= 1
else:
res.append([left_val, right_val])
while left < right and nums[left] == left_val:
left += 1
while left < right and nums[right] == right_val:
right -= 1
else:
for i in range(start, sz):
if i > start and nums[i] == nums[i - 1]:
continue
sub = self.nSumTarget(nums, n - 1, i + 1, target - nums[i])
for arr in sub:
res.append([nums[i]] + arr)
return res
七、用一个例子走一遍
假设:
nums = [-1, 0, 1, 2, -1, -4]
先排序:
[-4, -1, -1, 0, 1, 2]
调用:
nSumTarget(nums, 3, 0, 0)
也就是找 3Sum = 0。
第一层递归会依次固定一个数。
固定 -4:
剩下要找 2 个数,目标和是 4
在 [-1, -1, 0, 1, 2] 里找不到和为 4 的两数。
固定第一个 -1:
剩下要找 2 个数,目标和是 1
可以找到:
[-1, 2]
[0, 1]
拼回固定的 -1,得到:
[-1, -1, 2]
[-1, 0, 1]
继续到第二个 -1 时,因为它和前一个 -1 相同,并且处于同一层递归,所以跳过:
if i > start and nums[i] == nums[i - 1]:
continue
最终结果就是:
[[-1, -1, 2], [-1, 0, 1]]
八、复杂度分析
排序需要:
O(N log N)
2Sum 是:
O(N)
3Sum 是固定一个数,然后做 2Sum:
O(N^2)
4Sum 是固定一个数,然后做 3Sum:
O(N^3)
所以一般来说,nSum 的时间复杂度是:
O(N^(n-1))
递归深度是 n,所以除结果集以外,额外空间复杂度大约是:
O(n)
如果把输出结果也算进去,空间复杂度还要加上结果集本身的大小。
九、可以加的优化:剪枝
如果想进一步优化,可以在递归时加入剪枝。
因为数组已经排序,所以可以提前判断:
最小的 n 个数之和 > target,后面不用找了
最大的 n 个数之和 < target,后面也不用找了
简单写法如下:
if start + n > sz:
return res
min_sum = sum(nums[start:start + n])
max_sum = sum(nums[sz - n:sz])
if min_sum > target or max_sum < target:
return res
这不是模板成立的必要条件,但在数据量较大时能减少很多无效递归。
总结
这道题最重要的不是背代码,而是理解递归降维:
nSum = 固定一个数 + (n-1)Sum
一直降到:
2Sum = 双指针
再配合排序后的去重逻辑,就能统一解决 3Sum、4Sum 以及更一般的 KSum 问题。
可以把它记成一个四步模板:
- 排序。
- 如果
n == 2,用双指针。 - 如果
n > 2,枚举并固定一个数。 - 对剩余区间递归求
(n-1)Sum,拼接结果并去重。
这就是 nSum 问题最核心、最通用的解法框架。

nSum问题如何去解决
浙公网安备 33010602011771号