454.四数相加II
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。
例如:
输入:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]
输出:
2
解释:
两个元组如下:
- (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
- (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0
建议:本题是 使用map 巧妙解决的问题,好好体会一下 哈希法 如何提高程序执行效率,降低时间复杂度,当然使用哈希法 会提高空间复杂度,但一般来说我们都是舍空间 换时间, 工业开发也是这样。
题目链接/文章讲解/视频讲解:https://programmercarl.com/0454.%E5%9B%9B%E6%95%B0%E7%9B%B8%E5%8A%A0II.html
为什么使用hashmap来实现?
①高效查找
hashmap提供了常数时间的查找(即时间复杂度为O(1))使用哈希表可以显著减少查找操作的时间。
②避免重复计算
使用哈希表可以事先计算出
𝐴[𝑖]+𝐵[𝑗] 的所有组合,并将其存储下来,然后在查找 C[k]+D[l] 时可以直接用 O(1) 时间进行查询。
③ 简化代码实现
利用哈希表将问题分解为两个部分:先计算 A[i]+B[j] 的和,然后存入哈希表中,接着计算 C[k]+D[l] 时,直接查找哈希表中是否存在对应的值。
Map<Integer, Integer> 用来存储和计数。**具体来说:
- key:A[i] + B[j] 的和
在哈希表中,key 代表了所有可能的 A[i]+B[j] 的和。也就是说,key 是 A[i]+B[j] 的每个结果。我们通过哈希表的 put() 方法将这些和作为 key 存储。 - value:A[i] + B[j] 的和出现的次数
value 表示某个特定和的出现次数。因为 A[i]+B[j] 可能有多个组合的和相等,所以我们需要记录每个和出现的次数。在哈希表中,value 记录的是该和出现的次数。
import java.util.HashMap;
import java.util.Map;
//引入 Java 的 HashMap 类和 Map 接口。
//HashMap 是一个基于哈希表的实现,它提供了高效的键值对存储和查找操作,而 Map 是 HashMap 的接口类型。
public class FourSumCount {
public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
// 使用HashMap来存储 A[i] + B[j] 的和及其出现次数
Map<Integer, Integer> sumAB = new HashMap<>();//创建一个哈希表 sumAB,键是A[i]+B[j] 的和,值是该和出现的次数。
// 计算所有 A[i] + B[j] 的和,并记录出现次数
for (int a : A) {
for (int b : B) {//两个嵌套的 for 循环遍历数组 A 和 B 中的每一对元素 A[i] 和 B[j]。
sumAB.put(a + b, sumAB.getOrDefault(a + b, 0) + 1);
}
}
int count = 0;
// 对于每个 C[k] + D[l],查找是否存在 -(C[k] + D[l]) 在 sumAB 中
for (int c : C) {
for (int d : D) {
// 查找 sumAB 中是否有 - (c + d) 的键
count += sumAB.getOrDefault(-(c + d), 0);
}
}
return count;
}
public static void main(String[] args) {
FourSumCount solution = new FourSumCount();
// 测试数据
int[] A = {1, 2};
int[] B = {-2, -1};
int[] C = {-1, 2};
int[] D = {0, 2};
int result = solution.fourSumCount(A, B, C, D);
System.out.println(result); // 输出结果
}
}
sumAB.put(a + b, sumAB.getOrDefault(a + b, 0) + 1);//将计算出来的和 a+b 存储到哈希表 sumAB 中,并记录它出现的次数。
- put(key, value) 是 HashMap 类的一个方法, 用于将一个键(key)与其对应的值(value)存入哈希表。如果哈希表中已经存在该键,put 会更新该键对应的值;如果该键不存在,则会将该键值对加入哈希表。
- 在这里,key 是 a + b(即数组 A 和 B 中任意元素的和),value 是该和出现的次数。
sumAB.getOrDefault(a + b, 0)
- getOrDefault 方法会先检查哈希表中是否存在键 a + b:如果存在,返回对应的值,即该和出现的次数。如果不存在,返回默认值 0,表示该和尚未出现过。
- 这个方法的作用是确保在存储和之前的和时,不会抛出 NullPointerException 错误,即使这个和是第一次出现。
for (int c : C) {
for (int d : D) {
count += sumAB.getOrDefault(-(c + d), 0);
}
}
- 两个嵌套的 for 循环遍历数组 C 和 D 中的每一对元素 C[k] 和 D[l]。计算每对元素的和。
- 使用 sumAB.getOrDefault(-(c + d), 0) 查找哈希表中是否存在与 −(C[k]+D[l]) 相等的和:将返回的次数加到 count 中,这表示找到了多少个符合条件的元组。如果没有,返回 0。
383. 赎金信
建议:本题 和 242.有效的字母异位词 是一个思路 ,算是拓展题
题目链接/文章讲解:https://programmercarl.com/0383.%E8%B5%8E%E9%87%91%E4%BF%A1.html
本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成,但是这里需要注意两点。
- 第一点“为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思” 这里说明杂志里面的字母不可重复使用。
- 第二点 “你可以假设两个字符串均只含有小写字母。” 说明只有小写字母,这一点很重要
因为题目说只有小写字母,那可以采用空间换取时间的哈希策略,用一个长度为26的数组来记录magazine里字母出现的次数。
然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
依然是数组在哈希法中的应用。
一些同学可能想,用数组干啥,都用map完事了,其实在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
// shortcut
if (ransomNote.length() > magazine.length()) {
return false;
}
// 定义一个哈希映射数组,用于记录 magazine 中每个字符出现的次数。因为题目中只涉及小写字母('a' 到 'z'),所以数组大小为 26。
int[] record = new int[26];
// 遍历 magazine,更新字符频率:
for(char c : magazine.toCharArray()){
record[c - 'a'] += 1;
//每遇到一个字符,就在 record 数组中对应位置的值加 1,表示该字符在 magazine 中出现的次数。
}
//遍历 ransomNote,减少字符频率:
for(char c : ransomNote.toCharArray()){
record[c - 'a'] -= 1;//对于每个字符 c,我们从 record 数组中对应位置的值减 1,表示从 magazine 中“取走”了一个字符。
}
// 如果数组中存在负数,说明ransomNote字符串中存在magazine中没有的字符
for(int i : record){
if(i < 0){
return false;
}
}
return true;
}
}
时间复杂度:
遍历 magazine 和 ransomNote 需要 O(N) 的时间,其中 N 是字符串的长度。
空间复杂度:
使用了一个大小为 26 的数组 record 来记录字符的频率,因此空间复杂度是 O(1)(常数空间),因为 record 数组的大小是固定的。
这道题为什么考虑用数组,不用map?
题目中涉及的字符范围是固定的,即只涉及小写字母 a 到 z,总共有 26 个字符。因此,使用大小为 26 的数组来记录字符的频率是一个非常合适的选择,因为每个字符都可以映射到一个固定的数组索引(a 对应索引 0,b 对应索引 1,依此类推)。
如果使用 Map(如 HashMap)来记录字符频率,我们需要使用字符作为键(char),并且需要将每个字符映射到一个整型值(表示字符的频率)。Map 会为每个键(字符)分配内存空间,此外它还会有额外的结构开销,比如散列表的存储开销。虽然 Map 在平均情况下提供 O(1) 的访问时间,但在最坏情况下(例如哈希冲突严重时),Map 的访问时间可能退化为 O(n)。
15. 三数之和
建议:本题虽然和 两数之和 很像,也能用哈希法,但用哈希法会很麻烦,双指针法才是正解,可以先看视频理解一下 双指针法的思路,文章中讲解的,没问题 哈希法很麻烦。
题目链接/文章讲解/视频讲解:https://programmercarl.com/0015.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.html
哈希解法
两层for循环就可以确定 a 和b 的数值了,可以使用哈希法来确定 0-(a+b) 是否在 数组里出现过,其实这个思路是正确的,但是我们有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组。
把符合条件的三元组放进vector中,然后再去重,这样是非常费时的,很容易超时,也是这道题目通过率如此之低的根源所在。
去重的过程不好处理,有很多小细节,如果在面试中很难想到位。
时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。
用哈希法需要考虑去重操作,比较麻烦
使用排序+双指针法
首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。
接下来如何移动left 和right呢?
如果nums[i] + nums[left] + nums[right] > 0,说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0, 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
时间复杂度:O(n^2)。
== 考虑去重!==
主要考虑三个数的去重。 a, b ,c, 对应的就是 nums[i],nums[left],nums[right]
a 如果重复了怎么办?
a是nums里遍历的元素,那么应该直接跳过去。
但这里有一个问题,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同。都是和 nums[i]进行比较,是比较它的前一个,还是比较它的后一个。
如果我们的写法是 这样:
if (nums[i] == nums[i + 1]) { // 去重操作
continue;
}
那我们就把 三元组中出现重复元素的情况直接pass掉了。 例如{-1, -1 ,2} 这组数据,当遍历到第一个-1 的时候,判断 下一个也是-1,那这组数据就pass了。
我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!
所以这里是有两个重复的维度。
那么应该这么写:
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
这么写就是当前使用 nums[i],我们判断前一位是不是一样的元素,在看 {-1, -1 ,2} 这组数据,当遍历到 第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。
b与c的去重
去重的逻辑要多加对right 和left 的去重:
while (right > left) {
if (nums[i] + nums[left] + nums[right] > 0) {
right--;
// 去重 right
while (left < right && nums[right] == nums[right + 1]) right--;
} else if (nums[i] + nums[left] + nums[right] < 0) {
left++;
// 去重 left
while (left < right && nums[left] == nums[left - 1]) left++;
} else {
}
}
18. 四数之和
建议: 要比较一下,本题和 454.四数相加II 的区别,为什么 454.四数相加II 会简单很多,这个想明白了,对本题理解就深刻了。 本题 思路整体和 三数之和一样的,都是双指针,但写的时候 有很多小细节,需要注意,建议先看视频。
题目链接/文章讲解/视频讲解:https://programmercarl.com/0018.%E5%9B%9B%E6%95%B0%E4%B9%8B%E5%92%8C.html
import java.util.*;
public class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums); // 排序数组
List<List<Integer>> result = new ArrayList<>(); // 结果集
for (int k = 0; k < nums.length; k++) {
// 剪枝处理
if (nums[k] > target && nums[k] >= 0) {
break;
}
// 对nums[k]去重
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
for (int i = k + 1; i < nums.length; i++) {
// 第二级剪枝
if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
break;
}
// 对nums[i]去重
if (i > k + 1 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left) {
long sum = (long) nums[k] + nums[i] + nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
result.add(Arrays.asList(nums[k], nums[i], nums[left], nums[right]));
// 对nums[left]和nums[right]去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
}
return result;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 0, -1, 0, -2, 2};
int target = 0;
List<List<Integer>> results = solution.fourSum(nums, target);
for (List<Integer> result : results) {
System.out.println(result);
}
}
}
四数之和,——>使用双指针法
不要判断nums[k] > target 就返回了,三数之和 可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。
比如:数组是[-4, -3, -2, -1],target是-10,不能因为-4 > -10而跳过。
但是我们依旧可以去做剪枝,逻辑变成nums[i] > target && (nums[i] >=0 || target >= 0)就可以了。
1. fourSum 方法:
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums); // 排序数组
List<List<Integer>> result = new ArrayList<>(); // 结果集
Arrays.sort(nums); 首先对输入数组 nums 进行排序。这一步非常关键,因为排序后的数组使得后续的双指针方法能够高效地工作。
List<List> result = new ArrayList<>();:创建一个结果集 result,用于存储最终满足条件的四元组。
2. 外层循环:
for (int k = 0; k < nums.length; k++) {
// 剪枝处理
if (nums[k] > target && nums[k] >= 0) {
break;
}
// 对nums[k]去重
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
- 外层循环的作用:k 是用来固定第一个数字的索引。循环遍历整个数组,通过 nums[k] 来确定第一个数。
- 剪枝处理:在某些情况下,如果当前数已经大于目标值并且是正数(因为数组已经排序,后面的数更大),那么就可以提前结束循环,避免不必要的计算。这通过if (nums[k] > target && nums[k] >= 0) 来实现。
- 去重:为了避免重复的四元组,在每次选择nums[k] 时,检查当前的 nums[k] 是否与前一个数相同。如果相同,就跳过当前数,避免重复计算。
3. 第二层循环:
for (int i = k + 1; i < nums.length; i++) {
// 第二级剪枝
if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
break;
}
// 对nums[i]去重
if (i > k + 1 && nums[i] == nums[i - 1]) {
continue;
}
- 第二层循环的作用:i 是用来固定第二个数字的索引。此循环从 k + 1 开始,避免选择重复的数。
- 剪枝处理:在选择第二个数 nums[i] 时,如果当前 nums[k] + nums[i] 已经大于目标值并且是正数(后续的数值会更大),就可以提前结束循环。
- 去重:同样地,如果当前的 nums[i] 与前一个数 nums[i - 1] 相同,则跳过当前的 i,避免重复。
4. 双指针:
int left = i + 1;
int right = nums.length - 1;
while (right > left) {
long sum = (long) nums[k] + nums[i] + nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
result.add(Arrays.asList(nums[k], nums[i], nums[left], nums[right]));
// 对nums[left]和nums[right]去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
双指针的作用:在确定了前两个数字 nums[k] 和 nums[i] 后,通过使用两个指针 left 和 right 来遍历数组的剩余部分,寻找满足条件的第三个和第四个数。
- left 从 i + 1 开始,right 从数组的最后一个元素开始。
- sum 是当前四个数之和:nums[k] + nums[i] + nums[left] + nums[right]。
- 如果 sum 大于目标值,则移动 right 指针向左缩小总和;如果 sum 小于目标值,则移动 left 指针向右增大总和。
- 如果 sum 等于目标值,则将当前四元组添加到结果集 result 中,并继续移动 left 和 right 指针,同时避免重复。
5. 去重处理:
在添加四元组后,通过 while (right > left && nums[right] == nums[right - 1]) right–; 和 while (right > left && nums[left] == nums[left + 1]) left++; 来跳过重复的数,确保结果集中的四元组唯一。
6. 返回结果:
return result;
最后,返回包含所有满足条件的四元组的 result 列表。
7.main方法
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 0, -1, 0, -2, 2};
int target = 0;
List<List<Integer>> results = solution.fourSum(nums, target);
for (List<Integer> result : results) {
System.out.println(result);
}
}
main 方法用于测试 fourSum 方法。给定数组 nums = {1, 0, -1, 0, -2, 2} 和目标值 target = 0,调用 fourSum 方法并打印返回的所有四元组。
时间复杂度:排序的时间复杂度为 O(n log n),外层和内层循环的时间复杂度分别为 O(n),双指针部分的时间复杂度为 O(n)。因此,总的时间复杂度为 O(n^3)。
空间复杂度:主要是结果集的空间,最坏情况下空间复杂度为 O(n^3),不过实际情况下远小于这个值。
剪枝优化:通过提前退出循环和去重,有效减少了不必要的计算。