文章目录
- 思路
- 1.暴力算法(超出时间限制)
- ==解题思路==
- ==复杂度==
- 2.双指针算法
- ==解题思路:==
- ==注意点==
- ==复杂度==
Problem: 15. 三数之和
思路
1.暴力算法
2.双指针算法
1.暴力算法(超出时间限制)
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 检查输入数组是否为空或长度小于等于2,如果是,则返回空列表
if (nums == null || nums.length <= 2) {
return Collections.emptyList();
}
// 对输入数组进行排序
Arrays.sort(nums);
// 初始化结果列表
List<List<Integer>> ret = new ArrayList<>();
// 第一层循环遍历数组中的每个元素
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++) {
// 判断当前三元组是否满足和为0的条件
if (nums[i] + nums[j] + nums[k] == 0) {
// 创建一个新的列表并将满足条件的三元组添加到其中
List<Integer> value = new ArrayList<>();
value.add(nums[i]);
value.add(nums[j]);
value.add(nums[k]);
// 判断结果列表是否已经包含该三元组,如果不包含,则将其添加到结果列表中
if (!ret.contains(value)) {
ret.add(value);
}
}
}
}
}
// 返回包含满足条件的三元组的结果列表
return ret;
}
}
解题思路
暴力解法通过三层循环遍历数组中的所有三元组,并检查它们是否满足和为 0 的条件。当找到一个满足条件的三元组时,首先创建一个新的列表并将三个元素添加到其中。然后检查结果列表是否已经包含这个三元组,如果没有,则将其添加到结果列表中。最后,返回包含所有满足条件的不重复三元组的结果列表。
复杂度
- 时间复杂度:
暴力算法的时间复杂度主要来自于三层嵌套循环。由于每一层循环都需要遍历数组中的元素,所以暴力算法的时间复杂度为 O(n^3),其中 n 是输入数组的长度。
- 空间复杂度:
除了存储结果列表之外,暴力算法的空间复杂度较低。主要使用了几个变量(如 i、j 和 k)来控制循环。因此,算法的额外空间复杂度(除去结果列表)接近 O(1)。
2.双指针算法
class Solution {
public static List<List<Integer>> threeSum(int[] nums) {
// 对输入数组进行排序
Arrays.sort(nums);
// 创建一个 ArrayList 用于存储满足条件的三元组
List<List<Integer>> result = new ArrayList<>();
// 遍历数组,略过最后两个元素,因为我们需要至少三个元素来形成一个三元组
for (int i = 0; i < nums.length - 2; i++) {
// 如果当前元素大于0,不可能再找到和为0的三元组
if (nums[i] > 0) {
break;
}
// 跳过重复元素,以避免重复的三元组
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 初始化双指针,一个指针从当前元素的下一个开始,另一个指针从数组的最后一个元素开始
int left = i + 1;
int right = nums.length - 1;
// 当左指针小于右指针时,继续寻找满足条件的三元组
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
// 根据三数之和与0的关系,移动左指针或右指针
if (sum == 0) {
// 找到一个满足条件的三元组,添加到结果列表中
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 < 0) {
// 如果和小于0,需要增加和的值,因此将左指针向右移动
left++;
} else {
// 如果和大于0,需要减小和的值,因此将右指针向左移动
right--;
}
}
}
// 返回包含满足条件的三元组的结果列表
return result;
}
}
解题思路:
使用双指针方法优化搜索过程。首先,对数组进行排序,这将使得重复元素相邻并允许我们在搜索过程中轻松地跳过它们。接下来,遍历数组并使用双指针技巧查找和为 0 的三元组。对于当前元素 nums[i],使用一个指针 left 从 i+1 开始,另一个指针 right 从数组末尾开始。根据三数之和与 0 的关系来移动指针:
- 如果三数之和等于 0,找到一个满足条件的三元组。将其添加到结果列表中,然后跳过左右指针所指向的重复元素,最后同时移动左右指针。
- 如果三数之和小于 0,意味着需要增加和的值。因此,将左指针向右移动以获得更大的值。
- 如果三数之和大于 0,意味着需要减小和的值。因此,将右指针向左移动以获得较小的值。
在遍历过程中,我们可以使用一些优化方法,例如在当前元素大于 0 时提前终止循环,因为排序后的数组中,大于 0 的元素无法与其他元素形成和为 0 的三元组。此外,当遇到连续相同的元素时,可以直接跳过以避免添加重复的三元组。
通过这种方法,我们可以有效地找到数组中所有满足条件的不重复三元组。相比暴力解法,双指针方法大大降低了时间复杂度。
注意点
在这个解法中,遍历数组时,我们略过最后两个元素,这是因为我们需要至少三个元素来形成一个三元组。当我们将索引 i 定位在倒数第三个元素上时,双指针 left 和 right 将分别指向倒数第二个和最后一个元素。在这种情况下,我们仍然可以形成一个三元组(nums[i]、nums[left] 和 nums[right])。
如果我们将索引 i 定位在倒数第二个元素或最后一个元素上,那么双指针 left 和 right 将无法指向有效的数组元素,从而无法形成三元组。在这种情况下,继续遍历是没有意义的。
为了避免这种情况并确保我们能够在遍历过程中始终找到有效的三元组,我们在循环中设置了条件 i < nums.length - 2,这样在循环中,索引 i 最多只会到达数组的倒数第三个元素。
复杂度
- 时间复杂度:O(n^2)
双指针算法首先对数组进行排序,排序的时间复杂度为 O(nlogn)。接下来,它遍历数组的每个元素,并对每个元素使用双指针方法寻找满足条件的三元组。这一部分的时间复杂度为 O(n2)。因此,总的时间复杂度为 O(nlogn + n2)。在实际应用中,n2 项通常比 n*logn 项更显著,所以我们通常表示双指针算法的时间复杂度为 O(n2)。
- 空间复杂度:O(m)
双指针算法的空间复杂度主要取决于存储结果的列表。在实际应用中,空间复杂度会受到满足和为 0 的三元组数量的影响。假设有 m 个满足条件的三元组,那么空间复杂度为 O(m)。除此之外,双指针算法使用了几个变量(如 i、left 和 right)来控制循环,因此算法的额外空间复杂度(除去结果列表)接近 O(1)。