LeetCode 18 - 四数之和 详解笔记

1. 问题描述

1.1 题目要求

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a, b, c, d,使得 a + b + c + d = target?找出所有满足条件且不重复的四元组。

1.2 关键约束

  • 答案中不可以包含重复的四元组
  • 四元组的顺序不重要([a,b,c,d][b,a,c,d] 视为相同)
  • 需要处理整数溢出问题(数组元素和可能超出int范围)

1.3 示例

java 复制代码
示例1:
输入: nums = [1,0,-1,0,-2,2], target = 0
输出: [[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例2:
输入: nums = [2,2,2,2,2], target = 8
输出: [[2,2,2,2]]

示例3:
输入: nums = [1,-2,-5,-4,-3,3,3,5], target = -11
输出: [[-5,-4,-3,1]]

2. 算法思路

2.1 核心策略:排序 + 递归 + 双指针

整体框架采用分治思想,将复杂问题逐步简化:

  1. 排序预处理:对数组排序,便于后续去重和双指针查找
  2. 递归降维:将"n数之和"问题递归转化为"2数之和"问题
  3. 双指针求解:在2数之和中使用双指针法,O(n)时间复杂度求解

2.2 算法流程图

复制代码
四数之和问题
    ↓
排序数组
    ↓
固定第一个数(遍历)
    ↓
固定第二个数(递归)
    ↓
双指针求解剩余两数
    ↓
收集结果 + 去重

3. 核心方法详解

3.1 主方法 fourSum()

java 复制代码
static List<List<Integer>> fourSum(int[] nums, int target) {
    Arrays.sort(nums); // 关键:先排序
    List<List<Integer>> result = new LinkedList<>();
    dfs(4, 0, nums.length - 1, target, nums, new LinkedList<>(), result);
    return result;
}

作用:入口函数,负责排序和启动递归

3.2 递归方法 dfs() - n数之和通用解

3.2.1 方法签名
java 复制代码
static void dfs(int n, int i, int j, long target, int[] nums,
                LinkedList<Integer> stack, List<List<Integer>> result)
3.2.2 递归终止条件
java 复制代码
if (n == 2) {
    twoSum(i, j, nums, target, stack, result);
    return;
}

当问题规模缩小到2数之和时,转为双指针法求解

3.2.3 递归过程与剪枝优化
java 复制代码
for (int k = i; k < j - (n - 2); k++) {
    // 去重逻辑
    if (k > i && nums[k] == nums[k - 1]) continue;
    
    stack.push(nums[k]); // 做选择
    dfs(n - 1, k + 1, j, target - nums[k], nums, stack, result); // 递归
    stack.pop(); // 回溯
}

关键剪枝条件k < j - (n - 2)

原理

  • 需要确保当前位置k后面还至少有n-1个元素可选
  • 剩余元素个数 = j - k(从k+1到j的闭区间)
  • 要求:j - k >= n - 1k <= j - (n - 1)
  • 由于循环是k < ...(不包含边界),所以是j - (n - 2)

示例 :求4数之和,n=4,数组长度j=5

  • 循环边界:k < 5 - (4-2) = 3
  • k最大为2,剩余索引3、4 → 刚好2个元素,满足需求
3.2.4 去重机制
java 复制代码
if (k > i && nums[k] == nums[k - 1]) continue;
  • 跳过同一层级的重复元素
  • k > i 确保第一个元素不跳过(因为前面没有元素可比较)

3.3 双指针方法 twoSum()

3.3.1 方法签名
java 复制代码
static void twoSum(int i, int j, int[] numbers, long target,
                   LinkedList<Integer> stack, List<List<Integer>> result)
3.3.2 核心逻辑
java 复制代码
while (i < j) {
    long sum = numbers[i] + numbers[j]; // 用long避免溢出
    
    if (sum < target) i++;
    else if (sum > target) j--;
    else {
        // 找到解,构建结果
        ArrayList<Integer> list = new ArrayList<>(stack);
        list.add(numbers[i]);
        list.add(numbers[j]);
        result.add(list);
        
        i++;
        j--;
        
        // 跳过重复元素
        while (i < j && numbers[i] == numbers[i - 1]) i++;
        while (i < j && numbers[j] == numbers[j + 1]) j--;
    }
}
3.3.3 去重细节
  • 找到解后移动指针i++j--
  • 跳过左侧重复while (i < j && numbers[i] == numbers[i - 1]) i++;
  • 跳过右侧重复while (i < j && numbers[j] == numbers[j + 1]) j--;

4. 复杂度分析

项目 复杂度 说明
时间复杂度 O(n³) 排序O(nlogn) + 外层循环O(n) × 内层递归O(n) × 双指针O(n)
空间复杂度 O(logn) 递归栈空间(深度为n-2)+ 排序栈空间 + 结果存储

详细分析

  • 排序Arrays.sort() 使用快速排序,平均O(nlogn)
  • 递归:等效于三层嵌套循环(4数→3数→2数)
  • 双指针:O(n)线性扫描
  • 总体:主导项为O(n³),当n较大时排序开销可忽略

5. 关键技巧与细节

5.1 溢出处理

java 复制代码
// 方法参数使用 long
dfs(..., long target, ...) { ... }

// 计算时也使用 long
long sum = numbers[i] + numbers[j];
target - (long) nums[k] // 强制类型转换

必要性int最大值约21亿,两个大数相加容易溢出

5.2 栈的使用

java 复制代码
LinkedList<Integer> stack = new LinkedList<>();
stack.push(nums[k]);   // 入栈
stack.pop();           // 出栈(回溯)
new ArrayList<>(stack) // 创建副本加入结果

优势:天然支持回溯,自动维护已选元素

5.3 结果存储顺序

由于递归前序遍历栈的LIFO特性 ,最终结果中每个四元组的顺序是从后往前的,但题目不关心顺序,符合要求。


6. 完整代码实现

java 复制代码
package com.it.Y_Leetcode双指针;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

/**
 * LeetCode 18 - 四数之和
 * 
 * 算法思路:排序 + 递归 + 双指针
 * 时间复杂度:O(n³)
 * 空间复杂度:O(logn)
 */
public class SumLeetcode18 {

    public static void main(String[] args) {
        // 测试用例
        System.out.println(fourSum(new int[]{1, 0, -1, 0, -2, 2}, 0));
        System.out.println(fourSum(new int[]{2, 2, 2, 2, 2}, 8));
        System.out.println(fourSum(new int[]{1, -2, -5, -4, -3, 3, 3, 5}, -11));
        System.out.println(fourSum(new int[]{1000000000, 1000000000, 1000000000, 1000000000}, -294967296));
        System.out.println(fourSum(new int[]{-1000000000, -1000000000, 1000000000, -1000000000, -1000000000}, 294967296));
    }

    /**
     * 四数之和主方法
     * @param nums 输入数组
     * @param target 目标值
     * @return 所有不重复的四元组
     */
    static List<List<Integer>> fourSum(int[] nums, int target) {
        Arrays.sort(nums);
        List<List<Integer>> result = new LinkedList<>();
        dfs(4, 0, nums.length - 1, target, nums, new LinkedList<>(), result);
        return result;
    }

    /**
     * 递归求解 n 数之和
     * @param n 数字个数
     * @param i 起始索引
     * @param j 结束索引
     * @param target 目标值(long防溢出)
     * @param nums 排序后的数组
     * @param stack 已选数字栈
     * @param result 结果集
     */
    static void dfs(int n, int i, int j, long target, int[] nums,
                    LinkedList<Integer> stack, List<List<Integer>> result) {
        if (n == 2) {
            twoSum(i, j, nums, target, stack, result);
            return;
        }
        
        // 剪枝:确保剩余元素足够
        for (int k = i; k < j - (n - 2); k++) {
            // 去重
            if (k > i && nums[k] == nums[k - 1]) continue;
            
            stack.push(nums[k]);
            dfs(n - 1, k + 1, j, target - (long) nums[k], nums, stack, result);
            stack.pop();
        }
    }

    /**
     * 双指针法求两数之和
     * @param i 左指针
     * @param j 右指针
     * @param numbers 排序数组
     * @param target 目标值
     * @param stack 已选数字
     * @param result 结果集
     */
    static public void twoSum(int i, int j, int[] numbers, long target,
                              LinkedList<Integer> stack, List<List<Integer>> result) {
        while (i < j) {
            long sum = (long) numbers[i] + numbers[j];
            
            if (sum < target) {
                i++;
            } else if (sum > target) {
                j--;
            } else {
                // 找到解
                ArrayList<Integer> list = new ArrayList<>(stack);
                list.add(numbers[i]);
                list.add(numbers[j]);
                result.add(list);
                
                i++;
                j--;
                
                // 去重
                while (i < j && numbers[i] == numbers[i - 1]) i++;
                while (i < j && numbers[j] == numbers[j + 1]) j--;
            }
        }
    }
}

7. 测试用例分析

测试用例 数组 target 预期结果 测试目的
标准情况 [1,0,-1,0,-2,2] 0 3个四元组 基础功能
重复元素 [2,2,2,2,2] 8 [[2,2,2,2]] 去重能力
负数情况 [1,-2,-5,-4,-3,3,3,5] -11 [[-5,-4,-3,1]] 负数处理
大数溢出(正) [1e9,1e9,1e9,1e9] -294967296 [] 溢出防范
大数溢出(负) [-1e9,-1e9,1e9,-1e9,-1e9] 294967296 [] 溢出防范

8. 相关题目推荐

题目 难度 关联度 核心思路
LeetCode 1 - 两数之和 Easy ★★★★☆ 哈希表/双指针
LeetCode 15 - 三数之和 Medium ★★★★★ 排序+双指针(本题的简化版)
LeetCode 16 - 最接近的三数之和 Medium ★★★☆☆ 排序+双指针
LeetCode 454 - 四数相加 II Medium ★★★☆☆ 哈希表分组

9. 总结

9.1 算法精髓

  • 分治思想:将复杂问题层层分解,最终转化为简单问题
  • 空间换时间:通过排序和额外空间,将暴力O(n⁴)优化到O(n³)
  • 细节决定成败:溢出处理、去重逻辑、剪枝条件三个细节是AC关键

9.2 适用场景

本算法的递归框架 可推广到任意k数之和 问题,只需修改递归入口的n值即可。

9.3 常见错误

  1. ❌ 未处理int溢出 → 使用long
  2. ❌ 去重逻辑错误 → 注意k > ii < j条件
  3. ❌ 剪枝边界错误 → 牢记j - (n - 2)
相关推荐
Bug快跑-12 小时前
Java、C# 和 C++ 并发编程的深度比较与应用场景
java·开发语言·前端
2501_941111462 小时前
高性能计算集群部署
开发语言·c++·算法
AIpanda8882 小时前
AI销冠系统和AI提效软件系统是什么?主要特点和应用场景有哪些?
算法
受之以蒙2 小时前
具身智能的“任督二脉”:用 Rust ndarray 打通数据闭环的最后一公里
人工智能·笔记·rust
Moe4882 小时前
ConcurrentHashMap 重要方法实现原理和源码解析(二)
java·后端
普通网友2 小时前
模板编译期机器学习
开发语言·c++·算法
普通网友2 小时前
C++与机器学习框架
开发语言·c++·算法
普通网友2 小时前
C++安全编程指南
开发语言·c++·算法
有梦想的攻城狮2 小时前
初识Rust语言
java·开发语言·rust