三数之和问题是一个经典的算法面试题,要求在一个整数数组中找到所有不重复的三元组,使得这三个数的和恰好为零。这个问题看似简单,但包含了排序、双指针、去重等多个算法技巧的综合运用。
我们需要解决的问题可以分解为以下几个核心需求:
在实际面试中,面试官通常会期待候选人首先提出暴力解法,然后逐步优化到更高效的解决方案。这也是我们接下来要走的路径。
最直观的解法是三层循环遍历所有可能的三元组组合:
java复制// 暴力解法 - O(n³)时间复杂度
public List<List<Integer>> threeSumBruteForce(int[] nums) {
Set<List<Integer>> result = new HashSet<>();
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
for (int k = j + 1; k < nums.length; k++) {
if (nums[i] + nums[j] + nums[k] == 0) {
List<Integer> triplet = Arrays.asList(nums[i], nums[j], nums[k]);
Collections.sort(triplet); // 排序以便去重
result.add(triplet);
}
}
}
}
return new ArrayList<>(result);
}
这个解法虽然正确,但时间复杂度为O(n³),当n=3000时,运算量将达到270亿次,显然无法在合理时间内完成。我们需要更高效的算法。
优化的关键在于减少不必要的计算。我们可以采用以下策略:
这种方法的整体时间复杂度可以降到O(n²),对于n=3000的情况,运算量约为900万次,完全在可接受范围内。
让我们拆解优化解法的实现细节:
java复制public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
// 边界条件检查
if (nums == null || nums.length < 3) {
return result;
}
// 排序是后续所有优化的基础
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
// 优化1:如果当前数>0,后面不可能有解
if (nums[i] > 0) break;
// 跳过重复的固定数
if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1;
int right = nums.length - 1;
int target = -nums[i];
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 跳过重复的左元素
while (left < right && nums[left] == nums[left + 1]) left++;
// 跳过重复的右元素
while (left < right && nums[right] == nums[right - 1]) right--;
// 移动指针继续寻找
left++;
right--;
} else if (sum < target) {
left++; // 和太小,需要增大
} else {
right--; // 和太大,需要减小
}
}
}
return result;
}
排序的重要性:
双指针的移动逻辑:
去重处理:
java复制@Test
public void testThreeSum() {
Solution solution = new Solution();
// 普通情况
assertThat(solution.threeSum(new int[]{-1,0,1,2,-1,-4}))
.containsExactlyInAnyOrder(
Arrays.asList(-1,-1,2),
Arrays.asList(-1,0,1)
);
// 无解情况
assertThat(solution.threeSum(new int[]{0,1,1})).isEmpty();
// 全零情况
assertThat(solution.threeSum(new int[]{0,0,0}))
.containsExactly(Arrays.asList(0,0,0));
// 大数组测试
assertThat(solution.threeSum(new int[3000])) // 全0数组
.hasSize(1);
}
提前终止:
java复制if (nums[i] > 0) break;
当固定数大于0时,后面的数都更大,不可能有三数之和为0
跳过重复元素:
java复制if (i > 0 && nums[i] == nums[i - 1]) continue;
避免处理相同的固定数
双指针的微优化:
代码可读性:
防御性编程:
测试驱动开发:
类似的问题还有"最接近的三数之和",要求在数组中找到三个数,使它们的和最接近目标值。解法思路类似:
java复制public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int closestSum = nums[0] + nums[1] + nums[2];
for (int i = 0; i < nums.length - 2; i++) {
int left = i + 1;
int right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (Math.abs(sum - target) < Math.abs(closestSum - target)) {
closestSum = sum;
}
if (sum < target) {
left++;
} else if (sum > target) {
right--;
} else {
return sum; // 找到精确解
}
}
}
return closestSum;
}
进一步扩展,四数之和问题可以通过在三数之和的基础上再加一层循环来解决:
java复制public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
if (nums == null || nums.length < 4) return result;
Arrays.sort(nums);
for (int i = 0; i < nums.length - 3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue;
for (int j = i + 1; j < nums.length - 2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
int left = j + 1;
int right = nums.length - 1;
int remaining = target - nums[i] - nums[j];
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == remaining) {
result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
} else if (sum < remaining) {
left++;
} else {
right--;
}
}
}
}
return result;
}
三数之和算法在实际中有多种应用场景:
理解这类问题的解法不仅有助于通过技术面试,更能培养解决实际问题的算法思维。