很多人开始LeetCode的第一题就是求两数之和的问题,事实上除此之外,还有几个类似的问题,例如LeetCode15 三数之和,LeetCode18.四数相加和 LeetCode454.四数相加II等等。我们就集中看一下
1.两数之和
LeetCode1.给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那两个整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。
示例1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
该题目最容易想到的就是使用双重for循环进行解决,第一层确定一个数i,然后下一层继续遍历,找到目标值target - i的元素为止。
public static int[] twoSum(int[] nums, int target) {
int n = nums.length;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (nums[i] + nums[j] == target)
return new int[]{i,j};
}
}
return new int[0];
}
另外,我们还有比较节省时间复杂度的方法,使用哈希表Hash,对于查找target - x ,时间复杂度由O(n)降低为O(1)。
我们创建一个哈希表,判断遍历取得的target - x是否存在于哈希表中,如果存在,则返回对应 target - x 的下标,不存在,则将该 x,及其下标存入哈希表中,再进行遍历。
代码如下:
public static int[] twoSum2(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(target - nums[i])){
return new int[]{map.get(target - nums[i]),i};
}
map.put(nums[i],i);
}
return new int[0];
}
2.三数之和
如果将两个数换成三个会怎样呢?LeetCode15给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。注意:答案中不可以包含重复的三元组。
示例1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
本题看似就是增加了一个数,但是难度增加了很多,我们最先想到的方法应该就是三层f循环,但是该方法的时间复杂度太高了,因此不采用。
然后,我们想想可不可以用 双指针+Hash 来实现,首先按照第一题两数之和的思路,我们可以固定一个数target,再利用两数之和的思想去map中存取或查找(-1)*target - num[j],但是这样的问题是无法消除重复结果,例如如果输入[-1,0,1,2,-1,-4],返回的结果是[[-1,1,0],[-1,-1,2],[0,1,-1],[0,-1,1],[1,-1,0],[2,-1,-1]],如果我们再增加一个去重方法,将直接导致执行超时。
所以,我们使用上一题目的方法无法解决我们,我们就应该采用其他方法,公认比较好用的解法是使用 “排序 + 双指针”,我们可以先将数组排序来处理重复结果,然后还是固定一位元素,由于数组是排好序的,所以我们用双指针来不断寻找即可求解,代码如下:
public static List<List<Integer>> threeSum(int[] nums) {
int n = nums.length;
//先对数组进行排序(重要,否则后续操作无效)
Arrays.sort(nums);
//建立一个ArrayList来储存各种不同情况的List
List<List<Integer>> ans = new ArrayList<List<Integer>>();
// 枚举 a
for (int first = 0; first < n; ++first) {
// 需要和上一次枚举的数不相同
if (first > 0 && nums[first] == nums[first - 1]) {
continue;
}
// c 对应的指针初始指向数组的最右端
int third = n - 1;
int target = -nums[first];
// 枚举 b
for (int second = first + 1; second < n; ++second) {
// 需要和上一次枚举的数不相同
if (second > first + 1 && nums[second] == nums[second - 1]) {
continue;
}
// 需要保证 b 的指针在 c 的指针的左侧
while (second < third && nums[second] + nums[third] > target) {
--third;
}
// 如果指针重合,随着 b 后续的增加
// 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
if (second == third) {
break;
}
//当 c + b = -a 时,创建对应该三个元素的list,再将其存入ArrayList ans;
if (nums[second] + nums[third] == target) {
List<Integer> list = new ArrayList<Integer>();
list.add(nums[first]);
list.add(nums[second]);
list.add(nums[third]);
ans.add(list);
}
}
}
return ans;
}
3.四数之和
LeetCode18
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 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]]
对于该问题,我们可以参考上面 “排序 + 双指针”的方法进行思考。
虽然使用的方法相似,但是增加一个元素后,该问题在很多方面的考虑复杂了许多。
我们先使用双重for循环,对元素 a,b进行遍历,注意考虑枚举的数与上一次不要重复。
然后使用双指针方法:c 取 b + 1, 而 d 取 n - 1.并且取 c < d 为循环条件。
c,d双指针的循环部分是该题的难点,我们讨论一下具体实现:
- 先存储a+b+c+d元素的和sum,方便后续分情况进行讨论。
- 当sum为0时,将a,b,c,d处对应元素生成对应一个数组添加到ans(总数组)中,考虑到a,b元素一定时其他c,d元素组合的情况同时也要使枚举的数与上次不同,分别对c取++,d取–操作。
- 对于sum<0,以及sum>0的情况,分别取c++,d–.(当sum比0小,则说明c元素的值偏小,当sum比0大,则说明d元素的值偏大)
代码如下:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author 流连
* @version 1.0
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class FourSum {
public List<List<Integer>> fourSum(int[] nums) {
// 对数组进行排序,方便后续双指针查找
Arrays.sort(nums);
List<List<Integer>> ans = new ArrayList<>();
int n = nums.length;
// 四数之和,需要至少有4个元素,所以最外层循环只需到 n - 3
for (int a = 0; a < n - 3; a++) {
// 跳过重复的元素,确保四元组不重复
if (a > 0 && nums[a] == nums[a - 1]) {
continue;
}
// 第二层循环,选择第二个元素,类似于三数之和的思路
for (int b = a + 1; b < n - 2; b++) {
// 跳过重复的元素,确保四元组不重复
if (b > a + 1 && nums[b] == nums[b - 1]) {
continue;
}
// 双指针查找剩下的两个数,即第三个和第四个元素
int c = b + 1;
int d = n - 1;
while (c < d) {
// 计算当前四个数的和
int sum = nums[a] + nums[b] + nums[c] + nums[d];
if (sum == 0) {
// 如果和等于目标值,将四个数加入结果列表
ans.add(Arrays.asList(nums[a], nums[b], nums[c], nums[d]));
// 跳过重复的元素,确保四元组不重复
while (c < d && nums[c] == nums[c + 1]) {
c++;
}
while (c < d && nums[d] == nums[d - 1]) {
d--;
}
// 移动左指针和右指针,跳过相同的元素,继续查找下一个四元组
c++;
d--;
} else if (sum < 0) {
// 如果和小于目标值,说明需要增加和,移动左指针向右移动
c++;
} else {
// 如果和大于目标值,说明需要减少和,移动右指针向左移动
d--;
}
}
}
}
return ans;
}
public static void main(String[] args) {
int[] nums = {-6, -3, -1, 0, 2, 1, 10, 5};
List<List<Integer>> result = fourSum(nums);
System.out.println(result);
}
}