目录
1、两数之和
题目描述:
给定一个整数数组 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]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
解法一:暴力法
思路:
双重循环,遍历数组中的每一个数 x,寻找数组中是否存在 target - x。
代码:
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
for(int i=0;i<nums.length;i++){
for(int j=i+1;j<nums.length;j++){
if(nums[i]+nums[j]==target){
res[0]=i;
res[1]=j;
break;
}
}
}
return res;
}
}
解法二:Map
思路:
遍历数组,对于数组中的元素nums[i],计算其差值:target-nums[i]。如果map中包含差值,则说明存在两数之和等于target;如果不包含差值,则将当前数组元素和其下标存入map。
代码:
class Solution {
public int[] twoSum(int[] nums, int target) {
//存储map
Map<Integer, Integer> map = new HashMap<>();
//结果数组
int[] result = new int[2];
//遍历数组
for(int i=0; i < nums.length; i++){
//计算差值
int val = target-nums[i];
//如果map中包含差值,则说明存在两数之和等于target;如果不包含差值,则将当前数组元素和其下标存入map
if(map.containsKey(nums[i])){
result[1] = i;
result[0] = map.get(nums[i]);
break;
}else{
map.put(val, i);
}
}
return result;
}
}
2、两数之和Ⅱ:输入有序数组
题目描述:
给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。
以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]
解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。
示例 3:
输入:numbers = [-1,0], target = -1
输出:[1,2]
解释:-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
解法一:二分查找
思路:
在数组中找到两个数,使得它们的和等于目标值,可以首先固定第一个数,然后寻找第二个数,第二个数等于目标值减去第一个数的差。利用数组的有序性质,可以通过二分查找的方法寻找第二个数。为了避免重复寻找,在寻找第二个数时,只在第一个数的右侧寻找。
代码:
class Solution {
public int[] twoSum(int[] numbers, int target) {
int left = 0, right = numbers.length-1;
while(left < right){
//获取差值
int temp = target - numbers[left];
//当numbers[right]和numbers[left]的和大于target,right--
while(numbers[right] > temp){
right--;
}
//如果numbers[right]等于target,直接跳出
if(numbers[right] == temp){
break;
}
//左移
left++;
}
return new int[]{left+1, right+1};
}
}
解法二:双指针
思路:
初始时两个指针分别指向第一个元素位置和最后一个元素的位置。每次计算两个指针指向的两个元素之和,并和目标值比较。如果两个元素之和等于目标值,则发现了唯一解。如果两个元素之和小于目标值,则将左侧指针右移一位。如果两个元素之和大于目标值,则将右侧指针左移一位。移动指针之后,重复上述操作,直到找到答案。
代码:
class Solution {
public int[] twoSum(int[] numbers, int target) {
int low = 0, high = numbers.length - 1;
while (low < high) {
int sum = numbers[low] + numbers[high];
if (sum == target) {
return new int[]{low + 1, high + 1};
} else if (sum < target) {
++low;
} else {
--high;
}
}
return new int[]{-1, -1};
}
}
对于前两道题,两数之和数组有序和无序的思考:
(1)两数之和(数组无序):对于无序数组中查找是否存在两数之和等于target,要么就是双重循环查找,或者就是通过牺牲空间来换取时间,空间复杂度增加到O(n)。
(2)两数之和(数组有序):对于有序数组,就可以利用双指针或者二分法加快查找速度,且空间复杂度为O(1)。
(3)结合(1)和(2),如果对无序数组进行排序,然后再使用二分法或者双指针的方式也算一种方案,但这样排序算法最好的也需要O(nlogn)的时间复杂度,所以如果真的采用排序+双指针/二分法的思想来解决无序数组中的两数之和问题,其意义不是特别大。
3、三数之和
题目描述:
给你一个包含 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]]
示例 2:
输入:nums = []
输出:[]
示例 3:
输入:nums = [0]
输出:[]
解法:排序+双指针
思路:
暴力破解的话需要三层for循环,其时间复杂度达到O(n^3)。因此,可以先对数组进行排序,然后利用有序数组中两数之和的双指针解决方式,找到数组中和为target的三个数字。题目要求结果集中不能出现重复的元组,对于排序后的数组,重复的元素都是相邻的元素,则只需保证相邻两次枚举的相同元素跳过本次判断即可。
代码:
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
//如果数组长度小于3,直接返回[]
if(nums.length < 3){
return result;
}
//对数组进行排序
Arrays.sort(nums);
for(int i=0; i < nums.length-2; i++){
//如果当前元素大于0,则其后面所有元素都大于0,那么就不存在后续元素和为0的情况,直接跳出循环
if(nums[i] > 0){
break;
}
//如果当前元素等于其前一元素的值,则跳过本次循环,相当于去重
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
//得到0-nums[i],即为两个元素的和
int target = -nums[i];
int left = i+1, right = nums.length-1;
//双指针
while(left < right){
//如果nums[left]和 nums[right]之和等于target
if(nums[left] + nums[right] == target){
List<Integer> list = new ArrayList<>();
//添加结果集
Collections.addAll(list, nums[i], nums[left], nums[right]);
result.add(list);
//继续向中间走
left ++;
right --;
//去重
while(left < right && nums[left] == nums[left-1]){
left ++;
}
while(left < right && nums[right] == nums[right+1]){
right --;
}
}else if(nums[left] + nums[right] < target){ //如果nums[left]和 nums[right]之和小于于target,左指针右移
left ++;
}else{ ////如果nums[left]和 nums[right]之和大于于target,右指针左移
right --;
}
}
}
return result;
}
}
对于两数之和与三数之和带来的思考:
(1)借鉴于有序数组的两数之和,在求解三数之和时,可以对数组进行排序,然后再利用双指针的思想,在保证第一层循环的基础上,剩余的两个数就成为了求解有序数组的两数之和的问题。
(2)由于题目要求不重复的三元组,而在求解无序数组的两数之和问题时,通过将第一个元素放入HashMap中,即可解决元素重复的问题,然而HashMap这种方式在三数之和中并不适用。如果对数组进行排序之后,重复的元素都是相邻元素,那么在每一重循环时,通过判断相邻两个元素的值是否相同,即可达到去重的目的。
4、四数之和
题目描述:
给你一个由 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]]
解法:排序+双指针
思路:
四数之和和三数之和的解题思路一致,先对数组进行排序,然后加上双指针。相较于三数之和,本题多了一重循环。
(1)去重
循环过程中遵循以下两点:
-
每一种循环枚举到的数组下标必须大于上一重循环枚举到的数组下标;
-
同一重循环中,如果当前元素与上一个元素相同,则跳过当前元素。
(2)使用双指针
使用两重循环分别枚举前两个数,然后在两重循环枚举到的数之后使用双指针枚举剩下的两个数。假设两重循环枚举到的前两个数分别位于下标 i 和 j 其中 i<j。初始时,左右指针分别指向下标 j+1和下标 n-1。每次计算四个数的和,并进行如下操作:
- 如果和等于target,则将枚举到的四个数加到结果集中,然后将左指针右移直到遇到不同的数,将右指针左移直到遇到不同的数;
- 如果和小于target,则将左指针右移一位;
- 如果和大于target,则将右指针左移一位。
(3)优化操作
- 在确定第一个数之后,如果nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target,说明此时剩下的三个数无论取什么值,四数之和一定大于target,因此退出第一重循环;
- 在确定第一个数之后,如果nums[i]+nums[n−3]+nums[n−2]+nums[n−1]<target,说明此时剩下的三个数无论取什么值,四数之和一定小于target,因此第一重循环直接进入下一轮,枚举nums[i+1];
- 在确定前两个数之后,如果nums[i]+nums[j]+nums[j+1]+nums[j+2]>target,说明此时剩下的两个数无论取什么值,四数之和一定大于target,因此退出第二重循环;
- 在确定前两个数之后,如果nums[i]+nums[j]+nums[n−2]+nums[n−1]<target,说明此时剩下的两个数无论取什么值,四数之和一定小于target,因此第二重循环直接进入下一轮,枚举nums[j+1]。
代码:
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> quadruplets = new ArrayList<List<Integer>>();
//处理特殊情况
if (nums == null || nums.length < 4) {
return quadruplets;
}
//对数组进行排序
Arrays.sort(nums);
int length = nums.length;
//第一层循环
for (int i = 0; i < length - 3; i++) {
//当前元素等于其前一元素的值,跳过本次循环
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
//如果从当前元素向后的四数之和已经大于target,则说明后续元素就不会存在和为target的情况,直接结束循环
if ((long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
break;
}
//如果当前元素与数组后三个元素的和小于target,则说明本次循环不存在和为target的情况,跳过本次判断
if ((long) nums[i] + nums[length - 3] + nums[length - 2] + nums[length - 1] < target) {
continue;
}
for (int j = i + 1; j < length - 2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
//与前面同理
if ((long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
break;
}
if ((long) nums[i] + nums[j] + nums[length - 2] + nums[length - 1] < target) {
continue;
}
int left = j + 1, right = length - 1;
while (left < right) {
int sum = nums[i] + nums[j] + nums[left] + nums[right];
//存在结果集
if (sum == target) {
quadruplets.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
//去重
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
left++;
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
right--;
} else if (sum < target) { //小于,则左指针右移
left++;
} else { //大于,右指针左移
right--;
}
}
}
}
return quadruplets;
}
}
总结:本题思路和三数之和的解题思路基本一致,只不过本题处理的情况更为复杂,需要考虑到很多的细节。
5、四数之和Ⅱ
题目描述:
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
- 0 <= i, j, k, l < n
- nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1、(0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2、 (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1
解法:分组+HashMap
思路:
使用HashMap,并对四个数组A、B、C、D进行两两分组。
对于 A 和 B,使用二重循环对它们进行遍历,得到所有A[i]+B[j] 的值并存入哈希映射中。对于哈希映射中的每个键值对,每个键表示一种A[i]+B[j],对应的值为A[i]+B[j] 出现的次数。
对于 C 和 D,同样使用二重循环对它们进行遍历。当遍历到 C[k]+D[l] 时,如果−(C[k]+D[l]) 出现在哈希映射中,那么将−(C[k]+D[l]) 对应的值累加进答案中。最终即可得到满足 A[i]+B[j]+C[k]+D[l]=0 的四元组数目。
代码:
class Solution {
public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
//将两个数组的所有和存入map
for (int u : A) {
for (int v : B) {
map.put(u + v, map.getOrDefault(u + v, 0) + 1);
}
}
int count = 0;
for (int u : C) {
for (int v : D) {
//如果map中包含-(c[i]+d[i]),count++
if (map.containsKey(-u - v)) {
count += map.get(-u - v);
}
}
}
return count;
}
}
四数之和的思考:
相比于前一题,本题的是在四个数组上的操作,而前一题的四数之和是在一个数组上的操作,且本题中只要求找到四个元素和为0的元素个数,而并没有说不能重复。因此,本题相较于四数之和Ⅰ,数组维度变多,因此需要采用分治的思想。
扩展点:
(1)Map的merge方法:把两个值处理之后的结果作为value去更新map中以key为键的值,最后再将这个值返回给调用者。
Map<Integer, String> map = new HashMap<>();
map.put(1, "str1");
map.put(2, "str2");
map.merge(3, "My", String::concat);
System.out.println(map); //result: {1=str1, 2=str2, 3=My}
(2)Map的getOrDefault方法:当Map中有这个key时,就使用这个key值;如果没有就使用默认值defaultValue。
HashMap<String, String> map = new HashMap<>();
map.put("name", "cookie");
map.put("age", "18");
map.put("sex", "女");
String name = map.getOrDefault("name", "random");
System.out.println(name);// cookie,map中存在name,获得name对应的value
int score = map.getOrDefault("score", 80);
System.out.println(score);// 80,map中不存在score,使用默认值80
6、总结
(1)HashMap
哈希表用在两数之和(无序数组)上, 哈希表的使用是为了降低时间复杂度, 缩减寻找第二个元素使用的时间,将时间复杂度由O(n)降为O(1),其中无序数组是哈希表使用的重要条件, 因为当数组有序后, 可以直接使用 双指针 的方法来降低时间复杂度。所以数组有序之后, 我们应该首选 双指针 的方法。
(2)分治+HashMap
该方法在四数之和Ⅱ中使用,目的是降低四维空间的空间维度。通过两两分组实现降维,然后计算4个元素和为0的元组个数。如下分析:
- HashMap存一个数组,如A。然后计算三个数组之和,如BCD。时间复杂度为:O(n)+O(n^3),得到O(n^3)。
- HashMap存三个数组之和,如ABC。然后计算一个数组,如D。时间复杂度为:O(n^3)+O(n),得到O(n^3)。
- HashMap存两个数组之和,如AB。然后计算两个数组之和,如CD。时间复杂度为O(n^2)+O(n^2),得到O(n^2)。
(3)双指针
使用双指针最重要的条件就是数组是有序的,要根据实际情况来判断数组是否需要进行排序。适用于三数之和和四数之和Ⅰ。
(4)场景总结
- 对于两数之和,看它是否是有序的, 如果是无序的就使用 哈希表 的方法, 如果是有序的, 就可以使用双指针的方法。
- 对于一个数组上的三数之和/四数之和等, 无论数组是否有序,都排序后使用 双指针 的方法。
- 对于多个数组之和的情况, 首先对它们进行分组来实现降维操作。 一般来说都是分为两个相等的小组,之后再使用哈希表的方法。