从两数之和到四数相加,一套方法论搞定 N 数之和问题
前言
如果你刷过 LeetCode,一定对「两数之和」不陌生。但面试官往往会得寸进尺:「那三数之和呢?」「四数之和呢?」「四数相加 II 呢?」
今天,我就用两道经典题目,带你彻底搞懂 N 数之和问题的通用解法。
一、三数之和:排序 + 双指针
1.1 题目理解
给你一个整数数组 nums,判断是否存在三元组
[nums[i], nums[j], nums[k]]满足i != j != k,且nums[i] + nums[j] + nums[k] = 0。返回所有不重复的三元组。
示例:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
1.2 暴力解法 vs 优化思路
最直观的想法是三重循环,时间复杂度 O(n³),面试官直接摇头。
核心优化思路:
- 先排序 → 让数组有序,便于去重和双指针移动
- 固定一个数
a,然后在剩余数组中用双指针找b + c = -a
1.3 为什么不用哈希表?
学完「两数之和」后,你可能会想:用哈希表存 b,找 c = target - b 不行吗?
可行,但不优雅。三大痛点:
| 痛点 | 说明 |
|---|---|
| 去重困难 | 哈希表不处理顺序,[-1,0,1] 和 [-1,1,0] 可能被重复记录,需要额外 Set 去重 |
| 空间更大 | 每固定一个 a 就要新建哈希表,空间 O(n) vs 双指针 O(1) |
| 无法剪枝 | 哈希表不依赖有序性,无法像双指针那样用 nums[i] > 0 提前终止 |
结论: 单数组 + 需要去重 → 排序 + 双指针是更优解。哈希表更适合「两数之和」和「四数相加 II」这类场景。
1.4 图解双指针过程
以 nums = [-4, -1, -1, 0, 1, 2] 为例:
排序后:[-4, -1, -1, 0, 1, 2]
↑ ↑ ↑
i left right
固定 i = 0, a = -4
目标:找 b + c = 4
left 指向 -1,right 指向 2,和为 1 < 4 → left++
1.5 完整代码逐行解析
var threeSum = function(nums) {
const result = [];
// 关键步骤1:排序
nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length; i++) {
// 剪枝:最小的数都大于0,三数之和不可能为0
if (nums[i] > 0) return result;
// 关键步骤2:对 a 去重
if (i > 0 && nums[i] === nums[i - 1]) continue;
let left = i + 1;
let right = nums.length - 1;
while (right > left) {
const sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
result.push([nums[i], nums[left], nums[right]]);
// 关键步骤3:对 b 和 c 去重
while (right > left && nums[right] === nums[right - 1]) right--;
while (right > left && nums[left] === nums[left + 1]) left++;
right--;
left++;
}
}
}
return result;
};
1.6 去重逻辑详解(面试必问)
| 去重位置 | 代码 | 原因 |
|---|---|---|
| a 去重 | nums[i] === nums[i-1] | 同一个 a 值只处理一次 |
| b 去重 | nums[left] === nums[left+1] | 找到解后,跳过相同的 b |
| c 去重 | nums[right] === nums[right-1] | 找到解后,跳过相同的 c |
时间复杂度: O(n²)(排序 O(n log n) + 双指针 O(n²))
空间复杂度: O(log n) ~ O(n)(排序所需空间)
二、四数相加 II:分组 + 哈希表
2.1 题目理解
给你四个整数数组 nums1、nums2、nums3、nums4,长度都是 n。请你计算有多少个元组
(i, j, k, l)满足:nums1[i] + nums2[j] + nums3[k] + nums4[l] = 0
示例:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
2.2 思路转变
这题和三数之和最大的区别:四个数组相互独立,不需要考虑去重,只需要统计组合数量。
核心思想:
- 将
A + B + C + D = 0转化为A + B = -(C + D) - 用哈希表存储
A + B的所有可能和及其出现次数 - 遍历
C + D,查找-(C + D)在哈希表中的次数
2.3 代码实现
var fourSumCount = function(nums1, nums2, nums3, nums4) {
const map = new Map();
// 第一步:统计 nums1 + nums2 的所有可能和
for (const a of nums1) {
for (const b of nums2) {
const sum = a + b;
map.set(sum, (map.get(sum) || 0) + 1);
}
}
let count = 0;
// 第二步:在 nums3 + nums4 中找补数
for (const c of nums3) {
for (const d of nums4) {
const target = -(c + d);
if (map.has(target)) {
count += map.get(target);
}
}
}
return count;
};
2.4 为什么这题用哈希表?
| 特性 | 三数之和 | 四数相加 II |
|---|---|---|
| 数组数量 | 1个数组 | 4个独立数组 |
| 是否可排序 | 可以 | 可以(但没必要) |
| 是否需要去重 | 需要 | 不需要 |
| 最优解法 | 排序 + 双指针 | 分组 + 哈希表 |
复杂度分析:
- 时间复杂度:O(n²)(两个双重循环)
- 空间复杂度:O(n²)(哈希表最多存 n² 个键值对)
三、方法论总结:N 数之和问题怎么破?
3.1 题目分类
| 题目类型 | 特点 | 推荐解法 | 核心技巧 |
|---|---|---|---|
| 两数之和 | 一个数组,找两数和为 target | 哈希表 O(n) | 空间换时间 |
| 三数之和 | 一个数组,三数和为 0,去重 | 排序 + 双指针 O(n²) | 排序创造有序性 |
| 四数之和 | 一个数组,四数和为 target,去重 | 排序 + 双指针 O(n³) | 三层循环 + 双指针 |
| 四数相加 II | 四个独立数组,不要求去重 | 分组 + 哈希表 O(n²) | 转化为两数之和 |
3.2 核心技巧速记
// 1. 去重模板
if (i > 0 && nums[i] === nums[i-1]) continue;
// 2. 双指针收缩
while (left < right) {
if (sum > target) right--;
else if (sum < target) left++;
else {
while (left < right && nums[left] === nums[left+1]) left++;
while (left < right && nums[right] === nums[right-1]) right--;
left++;
right--;
}
}
// 3. 哈希表分组计数
const map = new Map();
for (...) {
map.set(sum, (map.get(sum) || 0) + 1);
}
3.3 一句话记忆
单数组去重用双指针,多数组计数用哈希表。
结语
这两道题看似不同,实则体现了算法设计的两种重要思想:
- 三数之和:用排序创造有序性,用双指针降低时间复杂度,用指针移动天然去重
- 四数相加 II:用哈希表做分组映射,化四重循环为两重循环
下次面试遇到 N 数之和问题,你就可以自信地问面试官:
「请问是在一个数组里还是多个数组?需要去重吗?」
然后给出对应的最优解。
如果这篇文章对你有帮助,欢迎点赞、收藏、转发!