三数之和
数组 双指针 排序题目描述
给定一个整数数组 nums,找出数组中所有和为 0 的三元组 [nums[i], nums[j], nums[k]]。
要求:
- 返回的结果中不能包含重复的三元组。
- 三元组的顺序可以是任意的。
思路分析
这道题的核心难点在于:如何高效地找到所有满足条件的三元组,并避免产生重复结果。
暴力解法的局限
最直观的想法是三层循环暴力枚举所有可能的三元组,时间复杂度 O(n³)。但这种方法不仅效率低,而且去重逻辑复杂(需要用 Set 或者排序后判断),不适合大规模数据。
排序 + 双指针优化
通过将问题转化为"固定一个数,然后在剩余数组中寻找两数之和"的形式,可以将时间复杂度降低到 O(n²)。
算法核心思想
-
先排序:将数组从小到大排序,这样做有三个好处:
- 可以使用双指针技巧。
- 方便去重(相同的数字会相邻)。
- 可以提前终止循环(当固定的数字 > 0 时,后面都是正数,和不可能为 0)。
-
固定第一个数:外层循环遍历数组,固定第一个数
nums[i]。 -
双指针寻找另外两个数:在
i右侧的子数组中,用双指针从两端向中间收缩,寻找和为-nums[i]的两个数:- 左指针
L从i + 1开始。 - 右指针
R从数组末尾开始。 - 计算
sum = nums[i] + nums[L] + nums[R]:- 若
sum = 0,找到一个三元组,记录结果,并同时移动L和R。 - 若
sum < 0,说明和太小,需要增大,移动左指针L++。 - 若
sum > 0,说明和太大,需要减小,移动右指针R--。
- 若
- 左指针
去重策略
为了避免重复的三元组,需要在两个地方进行去重:
-
外层循环去重:如果当前数字与前一个数字相同,跳过,避免重复固定相同的第一个数。
-
内层循环去重:找到一组解后,跳过所有与当前
L和R相同的元素。
剪枝优化
由于数组已排序,当固定的第一个数 nums[i] > 0 时,后面的数字都大于 0,三个正数的和不可能为 0,可以直接退出循环。
算法步骤
- 如果数组长度小于 3,直接返回空数组。
- 对数组从小到大排序。
- 外层循环遍历数组(
i从 0 到n - 2):- 如果
nums[i] > 0,直接退出循环(剪枝)。 - 如果
nums[i]与前一个数字相同,跳过(去重)。 - 初始化双指针:
L = i + 1,R = n - 1。 - 内层循环,当
L < R时:- 计算
sum = nums[i] + nums[L] + nums[R]。 - 如果
sum = 0,记录结果,跳过重复元素,同时移动L和R。 - 如果
sum < 0,L++(需要增大和)。 - 如果
sum > 0,R--(需要减小和)。
- 计算
- 如果
- 返回结果数组。
示例代码
复杂度分析
-
时间复杂度:
O(n²)- 排序:
O(n log n)。 - 外层循环:
O(n)。 - 内层双指针:每次
O(n)。 - 总体:
O(n log n) + O(n²) = O(n²)。
- 排序:
-
空间复杂度:
O(log n)- 排序算法(如快速排序)的递归栈空间。
- 不计算结果数组的空间。
示例演示
示例 1
输入:nums = [-1, 0, 1, 2, -1, -4]
第一步:排序
第二步:遍历 + 双指针
输出:[[-1, -1, 2], [-1, 0, 1]]
示例 2
输入:nums = [0, 0, 0]
排序后仍为 [0, 0, 0]。
输出:[[0, 0, 0]]
示例 3
输入:nums = [0, 1, 1]
排序后仍为 [0, 1, 1]。
输出:[]
关键要点
- 排序是前提:排序后才能使用双指针,并方便去重和剪枝。
- 双指针的移动规则:根据和的大小决定移动哪个指针,这是算法的核心。
- 去重要在两个地方:外层循环跳过相同的固定值,内层循环跳过相同的
L和R。 - 剪枝优化:当固定的数字 > 0 时,直接退出,避免无效计算。