目录

题目描述

18. 四数之和

image-20230309212907587

题意分析

要找的是所有互不重复的四元组,四个元素取自互不相同的下标,且四数之和恰好等于 target。四元组内部的先后顺序、以及答案里各四元组之间的顺序都不作要求,但同一组数值只能出现一次。
约束给了两个信号:一是数组长度只有 200 量级,容忍 $O(n^3)$ 级别的枚举;二是 nums[i]target 都可以取到 32 位整型的两端,四个这样的数相加会冲出 32 位。
边界要事先想清楚:长度不足 4 时答案为空;数组可能整片都是相同值;「不重复」是按数值判定而不是按下标判定的,所以 [0,0,0,0] 只能产出一组答案。

解法:排序 + 双层固定 + 双指针

核心思路

四重枚举需要 $O(n^4)$。先排序后固定前两个数 nums[i]nums[j],问题就降为在右侧有序区间中寻找目标两数,可用双指针把后两层从 $O(n^2)$ 降到 $O(n)$;排序还让相同值相邻,去重无需额外集合。

对固定的 (i,j),未检查的候选始终位于 [left,right]。若四数和偏小,那么当前 left 与区间内任意右端搭配都不会更大到命中目标,可以排除它并右移;和偏大时同理排除 right。命中后同时移动两端并跳过相同值。

去重必须分层进行:i 只与上一轮 i 比,j 只在当前 i 下与上一轮 j 比,命中后再跳过左右指针刚使用的值。这样每组数值只产生一次,又不会误删需要使用多个相同元素的合法答案。求和使用 64 位整数,避免四个 32 位数相加溢出。

解题步骤

  1. 将数组升序排序,使双指针移动和相邻去重都有依据。
  2. 枚举 i,若 i > 0 && nums[i] == nums[i-1] 则跳过。
  3. 枚举 j,仅当 j > i+1 && nums[j] == nums[j-1] 时跳过;j == i+1 是本层第一次使用该值,不能去重。
  4. [j+1,n-1] 放置双指针。用 64 位计算总和:偏小移动 left,偏大移动 right
  5. 命中时记录四元组,同时收缩两端,再分别跳过与刚使用值相同的元素。

[-2,-1,0,0,1,2]target=0 最终得到 [-2,-1,1,2][-2,0,0,2][-1,0,0,1][0,0,0,0] 中相同值可以占四个位置,但答案只输出一次。

代码实现

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        Arrays.sort(nums);
        List<List<Integer>> res = new ArrayList<>();
        int n = nums.length;

        for (int i = 0; i < n - 3; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            for (int j = i + 1; j < n - 2; j++) {
                if (j > i + 1 && nums[j] == nums[j - 1]) {
                    continue;
                }
                int left = j + 1;
                int right = n - 1;
                while (left < right) {
                    // 使用 long 计算四数和,避免极值输入溢出。
                    long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
                    if (sum == target) {
                        res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                        left++;
                        right--;
                        while (left < right && nums[left] == nums[left - 1]) {
                            left++;
                        }
                        while (left < right && nums[right] == nums[right + 1]) {
                            right--;
                        }
                    } else if (sum < target) {
                        left++;
                    } else {
                        right--;
                    }
                }
            }
        }
        return res;
    }
}
import "sort"

func fourSum(nums []int, target int) [][]int {
    sort.Ints(nums)
    res := make([][]int, 0)
    n := len(nums)

    for i := 0; i < n-3; i++ {
        if i > 0 && nums[i] == nums[i-1] {
            continue
        }
        for j := i + 1; j < n-2; j++ {
            if j > i+1 && nums[j] == nums[j-1] {
                continue
            }
            left := j + 1
            right := n - 1
            for left < right {
                sum := int64(nums[i]) + int64(nums[j]) + int64(nums[left]) + int64(nums[right])
                if sum == int64(target) {
                    res = append(res, []int{nums[i], nums[j], nums[left], nums[right]})
                    left++
                    right--
                    for left < right && nums[left] == nums[left-1] {
                        left++
                    }
                    for left < right && nums[right] == nums[right+1] {
                        right--
                    }
                } else if sum < int64(target) {
                    left++
                } else {
                    right--
                }
            }
        }
    }
    return res
}

复杂度分析

  • 时间复杂度:$O(n^3)$。排序为 $O(n \log n)$,两层枚举与一轮线性双指针为 $O(n^3)$。
  • 空间复杂度:忽略返回结果,指针部分为 $O(1)$;计入语言排序实现的调用栈通常为 $O(\log n)$。

关键点总结

  • 排序同时解决双指针的单调性和按数值去重,是 nSum 问题的基础。
  • 去重是“同一层不重复选同一个值”,不是全局禁止重复值;否则 [0,0,0,0] 会被误删。
  • 命中后左右指针都要移动,并分别越过刚使用值的重复项。
  • 面试中要主动说明双指针为何不会漏解,以及求和为什么必须转成 64 位。

易错点总结

  • 用 32 位整数求四数和:极值输入会溢出,导致指针移动方向错误。
  • j 去重漏掉 j > i+1[0,0,0,0] 会把本层第一个 0 也跳过,丢失合法答案。
  • 命中后不跳过左右重复值:[2,2,2,2,2]target=8 会重复输出同一四元组。
  • 循环写成 left <= right:当两指针重合时会重复使用同一下标。
  • 未排序就移动双指针:和的变化失去单调性,移动任何一端都可能漏解。

相似题目

题目 难度 考察点
15. 三数之和 中等 少固定一层,去重只需处理三个位置
16. 最接近的三数之和 中等 求最接近而非相等,无需去重
167. 两数之和 II - 输入有序数组 中等 输入已有序,直接双指针并返回下标
259. 较小的三数之和 中等 统计满足小于关系的组合数而非列举
611. 有效三角形的个数 中等 固定最长边后用双指针批量计数
1099. 小于 K 的两数之和 简单 求不超过上界的最大两数和
LCR 006. 两数之和 II - 输入有序数组 简单 有序两数之和的下标版本
LCR 007. 三数之和 中等 三数之和换编号,去重要求完全一致
剑指 Offer 57. 和为s的两个数字 简单 有序数组中任取一组解即可
面试题 16.24. 数对和 中等 无序数组配对,可用哈希代替排序