代码随想录算法训练营第六天Leetcode454.四数相加II-383.-赎金信-15.-三数之和-18.-四数之和-
代码随想录算法训练营第六天|Leetcode454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和
15 三数之和
建议:本题虽然和 两数之和 很像,也能用哈希法,但用哈希法会很麻烦,双指针法才是正解,可以先看视频理解一下 双指针法的思路,文章中讲解的,没问题
哈希法很麻烦。
题目链接/文章讲解/视频讲解:
和之前我们遇到的两数之和不同,两数之和可以很快的通过哈希表完成作答
两层for循环就可以确定 两个数值,可以使用哈希法来确定 第三个数 0-(a+b) 或者 0 - (a + c) 是否在
数组里出现过,其实这个思路是正确的,但是我们有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组。
把符合条件的三元组放进set中,然后再去重,这样是非常费时的,很容易超时,也是这道题目通过率如此之低的根源所在。
所以我们在这里优先考虑双指针算法
我们首先先对数组进行排序,使其变成升序的方式,然后从第一位元素开始遍历数组,并设置两个指针
拿这个nums数组来举例,首先将数组排序,然后有一层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相遇为止。
我们来看代码
class Solution {
public List> threeSum(int[] nums) {
List> result = new ArrayList<>();
Arrays.sort(nums);
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.length; i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
if (i > 0 && nums[i] == nums[i - 1]) { // 去重a
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right–;
} else if (sum < 0) {
left++;
} else {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right–;
while (right > left && nums[left] == nums[left + 1]) left++;
right–;
left++;
}
}
}
return result;
}
}
这些代码里面有很多值得思考的地方
a的去重
说到去重,其实主要考虑三个数的去重。 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} 这组数据一样可以收录到 结果集里。
前面有-1了,那这样第一个数为-1的所有组合都找到了,然后再将第一个数的移到更大的一个数,再进行寻找,其实就是第一个数的去重,要将i设置为不同的数字才可以
b与c的去重
当我们找到了一组符合条件的a,b,c的时候我们需要对b,c进行去重,这里可以举个例子
在这个情况下,我们看到确定了i之后,如果移动left或者right会造成重复,
我们来看去重逻辑
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right–;
while (right > left && nums[left] == nums[left + 1]) left++;
right–;
left++;
18 四数之和
建议: 要比较一下,本题和 454.四数相加II 的区别,为什么 454.四数相加II 会简单很多,这个想明白了,对本题理解就深刻了。 本题 思路整体和
三数之和一样的,都是双指针,但写的时候 有很多小细节,需要注意,建议先看视频。
题目链接/文章讲解/视频讲解:
这个题目的思路和之前的三数之和基本一致,只是在剪枝的时候判断更加复杂一点,在三数之和中,target=0,所以我们在进行判断的时候,基于升序排序的数组,,如果当前位置的值的大小已经>0,那么直接break即可,
四数之和这道题目 target是任意值。比如:数组是
[-4, -3, -2, -1]
,target
是-10
,不能因为-4 > -10
而跳过。但是我们依旧可以去做剪枝,逻辑变成nums[k] > target && (nums[k] >=0 || target >= 0)
就可以了。
我们直接来看代码
import java.util.*;
public class Solution {
public List> fourSum(int[] nums, int target) {
Arrays.sort(nums); // 排序数组
List> result = new ArrayList<>(); // 结果集
for (int k = 0; k < nums.length; k++) {
// 剪枝处理
if (nums[k] > target && nums[k] >= 0) {
break; // 此处的break可以等价于return result;
}
// 对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; // 注意是break到上一级for循环,如果直接return result;会有遗漏
}
// 对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> results = solution.fourSum(nums, target);
for (List result : results) {
System.out.println(result);
}
}
}
383 赎金信
建议:本题 和 242.有效的字母异位词 是一个思路 ,算是拓展题
题目链接/文章讲解:
这道题思路和有效的字母移位词比较像,还是先遍历,后判断出现频率是否为0,在遍历的时候,我们可以先做判断,如果需要组成的字段的长度>给定的字段,我们直接返回false即可,最后判断的条件,也只需要是数组里的所有元素都大于等于0即可,如果小于0,说明给点字段里面存在不包含生成字段所需字母的情况。
我们来看代码
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
// shortcut
if (ransomNote.length() > magazine.length()) {
return false;
}
// 定义一个哈希映射数组
int[] record = new int[26];
// 遍历
for(char c : magazine.toCharArray()){
record[c - ‘a’] += 1;
}
for(char c : ransomNote.toCharArray()){
record[c - ‘a’] -= 1;
}
// 如果数组中存在负数,说明ransomNote字符串中存在magazine中没有的字符
for(int i : record){
if(i < 0){
return false;
}
}
return true;
}
}
454.四数相加II
建议:本题是 使用map 巧妙解决的问题,好好体会一下 哈希法 如何提高程序执行效率,降低时间复杂度,当然使用哈希法
会提高空间复杂度,但一般来说我们都是舍空间 换时间, 工业开发也是这样。
题目链接/文章讲解/视频讲解:
这道题思路和之前一样,因为之前几道题需要考虑去重,使用哈希表的话会非常复杂,
双指针技术可以在有序数组中高效地找到满足条件的元素对,而在哈希表中,这样的操作并不直观。
哈希表主要用于快速判断元素是否存在,而不是用于去重。虽然可以通过哈希表来检查元素是否已经出现过,但在去重时,我们通常需要知道元素出现的次数或者需要移除重复的元素,这并不是哈希表直接提供的功能。
因为四个数组,使用哈希表我们可以分成两组,遍历前两组数组,然后将每次遍历的结果储存起来,如果结果出现不止一次,我们通过
map.put(sum, map.getOrDefault(sum, 0) + 1);
将它的次数增加,便于我们直接返回元组个数
我们来看代码:
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
int res = 0;
Map map = new HashMap();
//统计两个数组中的元素之和,同时统计出现的次数,放入map
for (int i : nums1) {
for (int j : nums2) {
int sum = i + j;
map.put(sum, map.getOrDefault(sum, 0) + 1);
}
}
//统计剩余的两个元素的和,在map中找是否存在相加为0的情况,同时记录次数
for (int i : nums3) {
for (int j : nums4) {
res += map.getOrDefault(0 - i - j, 0);
}
}
return res;
}
}