📚 目录
-
[1. 题目解析](#1. 题目解析)
-
[2. 算法原理](#2. 算法原理)
-
[3. 编写代码](#3. 编写代码)
前置阅读:
Java双指针算法精讲(五)|LCR 179 有序数组两数之和 剑指Offer详解
1. 题目解析

题干核心总结 :
- 选取3个下标互不相同的数字,满足三者相加等于0;
- 三元组内数字调换顺序视为同一答案,结果集合必须去重;
- 解题转化思路:固定第一个数字,剩余两数求和等于
-nums[i],复用前文有序两数之和双指针模板; - 面试核心难点:不借助HashSet ,仅通过排序+指针原地跳过重复值实现去重。
[🔙 返回目录](#🔙 返回目录)
2. 算法原理
解决这一道题目:我们采取的方法还是双指针 ;
以此数组为例:

暴力解法:
使用三重循环进行枚举出所有情况,进行相加判断是否等于0,再通过hashset进行去重,最后放入到链表当中进行返回;
| 指标 | 复杂度 | 详细说明 |
|---|---|---|
| 时间复杂度 | O ( n 3 ) O(n^3) O(n3) | 三层嵌套循环枚举全部三元组,数据量稍大直接超时,无法通过LeetCode大数据用例 |
| 空间复杂度(不计结果集) | O ( 1 ) O(1) O(1) | 仅使用循环下标临时变量,无额外数组、哈希容器开辟 |
| 空间复杂度(含Set去重存储答案) | O ( n ) O(n) O(n) | 依赖HashSet存放结果实现去重,额外占用线性空间 |
双指针算法 :
首先给数组进行排序:
[-4 , -4 , -1 , -1 , -1 , 0 , 1 , 2] :排序后:

算法原理:

- 初始化指针:固定外层下标
i,区间左边界left = i+1,区间右边界right = nums.length - 1,在[left, right]区间内查找两数之和等于-nums[i],查找过程中,即使已经遇到了满足条件的数,也不要停下,直到left>=right的时候停; - 单次区间遍历结束后,
i向右移动进入下一轮循环; - 外层i指针去重 :若当前
nums[i]与前一个数字nums[i-1]相等,直接跳过本轮;
例:数组中两个-4,第二个-4作为i时,会和前一个-4生成完全一致的三元组,直接跳过避免重复结果; - 内层left、right指针去重 :匹配到一组和为0的三元组后,持续跳过左右侧相邻重复数字 ;
left指针:循环跳过后续和当前left相等的值,防止第二个数字重复;
right指针:循环跳过前面和当前right相等的值,防止第三个数字重复; - 重复整套逻辑,直至
i遍历到数组倒数第三个元素,全部组合枚举完成。
[🔙 返回目录](#🔙 返回目录)
3. 编写代码
边界处理:
- 外层固定指针 i 的边界去重
边界条件 :i > 0 && nums[i] == nums[i - 1]
- 下限边界
i>0:必须保证i存在前一位,防止数组下标越界; - 去重逻辑:若当前i值和上一轮数值相等,直接跳过本轮循环;
- 示例边界:数组连续两个
-4,第二个i=-4时触发判断,跳过重复首元素,避免生成完全重复三元组;
- 终止边界:i最大只能遍历到nums.length - 3,预留left、right两个指针位置。
- 左指针 left 的边界去重
边界条件 :left < right && nums[left] == nums[left + 1]
- 核心边界
left < right:必须保证左指针始终在右指针左侧,防止下标交叉越界; - 去重逻辑:匹配一组合法三元组后,持续右移left,跳过所有连续重复值;
- 临界场景:数组连续多个
-1,不加left < right会出现left+1 > right数组越界报错。
- 右指针 right 的边界去重
边界条件 :left < right && nums[right] == nums[right - 1]
- 边界约束left < right:和左指针共用同一安全边界,避免right持续左移跨过left;
- 去重逻辑:匹配成功后持续左移right,跳过右侧重复数值;
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
int len = nums.length;
List<List<Integer>> list = new LinkedList<>();
for (int i = 0; i < len-2; i++) {
if(i>0 && nums[i] == nums[i-1]) {
continue;
}
//定义两个指针:
int left = i+1;
int right = len-1;
while (left<right) {
int sum = nums[i] + nums[left] +nums[right];
List<Integer> ret = new LinkedList<>();
if(sum == 0) {
//添加到链表中
ret.add(nums[i]);
ret.add(nums[left]);
ret.add(nums[right]);
//添加完,放入集合
list.add(ret);
//边界处理
while (left<right && nums[left] == nums[left+1]) {
left++;
}
while (left<right && nums[right] == nums[right-1]) {
right--;
}
left++;
right--;
}else if(sum<0) {
left++;
}else {
right--;
}
}
}
return list;
}
}

易错点:
| 指针 | 边界判断条件 | 边界作用 | 越界风险 |
|---|---|---|---|
| 外层i | i > 0 |
保证存在前一位元素用于去重 | i=0时 i-1=-1,数组下标越界 |
| 左left | left < right |
保证左指针在右指针左侧 | left+1 > right,访问不存在数组下标 |
| 右right | left < right |
限制右指针不跨过左指针 | right-1 < left,下标反向越界 |
| 全局前置 | nums.length < 3 |
过滤无合法三元组场景 | 循环内i、left、right下标访问报错 |
时间、空间复杂度分析
| 指标 | 复杂度 | 详细说明 |
|---|---|---|
| 时间复杂度 | O ( n 2 ) O(n^2) O(n2) | 1. 排序开销 O ( n log n ) O(n\log n) O(nlogn); 2. 外层i单层循环遍历数组 O ( n ) O(n) O(n),内层双指针最多遍历一次剩余区间 O ( n ) O(n) O(n); n 2 \boldsymbol{n^2} n2 量级主导整体耗时,远优于暴力三层循环 O ( n 3 ) O(n^3) O(n3) |
| 额外空间复杂度 | O ( log n ) O(\log n) O(logn) | 仅排序递归栈占用空间; 结果集合List为题目输出要求,不计入额外空间;全程无HashSet、额外数组,无线性空间开销 |
[🔙 返回目录](#🔙 返回目录)