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<List<Integer>> threeSum(int[] nums) {List<List<Integer>> 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]) { // 去重acontinue;}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<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; // 此处的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<List<Integer>> results = solution.fourSum(nums, target);for (List<Integer> result : results) {System.out.println(result);}}
}
383. 赎金信
建议:本题 和 242.有效的字母异位词 是一个思路 ,算是拓展题
题目链接/文章讲解:代码随想录
这道题思路和有效的字母移位词比较像,还是先遍历,后判断出现频率是否为0,在遍历的时候,我们可以先做判断,如果需要组成的字段的长度>给定的字段,我们直接返回false即可,最后判断的条件,也只需要是数组里的所有元素都大于等于0即可,如果小于0,说明给点字段里面存在不包含生成字段所需字母的情况。
我们来看代码
class Solution {public boolean canConstruct(String ransomNote, String magazine) {// shortcutif (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<Integer, Integer> map = new HashMap<Integer, Integer>();//统计两个数组中的元素之和,同时统计出现的次数,放入mapfor (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;}
}