【算法训练营Day06】哈希表part2
文章目录
- 四数相加
- 赎金信
- 三数之和
- 四数之和
四数相加
题目链接:454. 四数相加 II
这个题注意它只需要给出次数,而不是元组。所以我们可以分治。将前两个数组的加和情况使用map存储起来,再将后两个数组的加和情况使用map存储起来,key存和,value存出现的次数。得到两个map之后,我们遍历其中一个map, 看另一个map中是否有和为0的情况,有就相加value,最后接可以得出答案。
在这种思路的基础上,我们可以继续优化代码,例如我们在统计后两个数组的同时,就已经可以将需要的和在前两个数组的map中找出来,然后把次数进行相加。
代码如下:
class Solution {public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {Map<Integer,Integer> records = new HashMap<>();for(int i = 0;i < nums1.length;i++) {for(int j = 0;j < nums2.length;j++) {records.put(nums1[i] + nums2[j],records.getOrDefault(nums1[i] + nums2[j],0) + 1);}}int result = 0;for(int i = 0;i < nums3.length;i++) {for(int j = 0;j < nums4.length;j++) {Integer count = records.get(0 - nums3[i] - nums4[j]);if(count != null) result += count;}}return result;}
}
赎金信
题目链接:383. 赎金信
二十六个字母,计数,第一反应就要想到数组。解题思路如下:
- 用数组统计第一个单词的个数
- 然后遍历第二个单词,在数组中相应位置进行减法运算
- 遍历数组
- 如果存在大于0的数返回false
- 不存在则返回true
class Solution {public boolean canConstruct(String ransomNote, String magazine) {int[] records = new int[26];for (int i = 0; i < ransomNote.length(); i++) records[ransomNote.charAt(i) - 'a']++;for (int i = 0; i < magazine.length(); i++) records[magazine.charAt(i) - 'a']--;for (int i = 0; i < records.length; i++) if(records[i] > 0 ) return false;return true;}
}
三数之和
题目链接:15. 三数之和
解题思路:
看到这一题我们肯定会不自觉地拿它和两数之和进行比较,我们是否能借助两数之和的思想来完成这一题?首先我们回顾一下两数之和的思想。在两数之和中,我们是遍历数组,每遍历一个元素,就看target - 该元素 是否已经出现过(也就是是否在hash表中),如果在直接返回,如果不在就把这个元素添加到hash表中,代表该元素出现过,为后面的元素服务。
在三数相加中,我们尝试沿用这种思路(先不直接到位,后面还会添加新逻辑):
- 使用双层for循环遍历数组,外层循环相当于固定一个数,内层for循环沿用两数相加的逻辑
- 初始化一个hashset,用来存已经存在的数,外层循环的以固定值不需要存
- 内存循环遍历数组,寻找0 - nums[head]- nums[end] 是否存在于hashset中
- 如果存在那么该数组添加到答案列表中
- 如果不存在继续遍历
- 外层循环每完成一次清空set
- 最后返回答案集合
代码如下:
class Solution {public List<List<Integer>> threeSum(int[] nums) {List<List<Integer>> result = new ArrayList<>();HashSet<Integer> set = new HashSet<>();for (int i = 0; i < nums.length; i++) {for (int j = i + 1; j < nums.length; j++) {int need = -nums[i] - nums[j];if (set.contains(need)) {result.add(Arrays.asList(nums[i], nums[j], need));} else {set.add(nums[j]);}}set.clear();}return result;}
}
这个代码完成了基本的功能但是还差本题的一个重点那就是去重。
就比如题目的这个用例:[-1,0,1,2,-1,-4]
如下两种情况就会重复:
- i指向-1,j指向1,set里有0,这组会返回
- i指向0,j指向-1,set里有1,这组也会返回
我们可以尝试排序解决这个问题:
排序之后还是这个用例就变为:[-4,-1,-1,0,1,2]
外层循环在固定到两个-1的时候肯定会发生重复,所以我们可以添加一个条件,外层循环固定的数字和上一次相同时直接跳过:
class Solution {public List<List<Integer>> threeSum(int[] nums) {Arrays.sort(nums);List<List<Integer>> result = new ArrayList<>();HashSet<Integer> set = new HashSet<>();for (int i = 0; i < nums.length; i++) {if (i > 0 && nums[i] == nums[i - 1]) {continue;}for (int j = i + 1; j < nums.length; j++) {int need = -nums[i] - nums[j];if (set.contains(need)) {result.add(Arrays.asList(nums[i], nums[j], need));} else {set.add(nums[j]);}}set.clear();}return result;}
}
改完之后发现这个测试用例过不了:
原因就是当内层的两数相加满足之后,内层的下一次循环还是相同的数,那么相当于把这一组答案又加了一遍,那么我们针对这个情况进行改进:
class Solution {public List<List<Integer>> threeSum(int[] nums) {Arrays.sort(nums);List<List<Integer>> result = new ArrayList<>();HashSet<Integer> set = new HashSet<>();for (int i = 0; i < nums.length; i++) {if (i > 0 && nums[i] == nums[i - 1]) {continue;}Integer flag = null;for (int j = i + 1; j < nums.length; j++) {if(Integer.valueOf(nums[j]).equals(flag)) continue;flag = null;int need = -nums[i] - nums[j];if (set.contains(need)) {result.add(Arrays.asList(nums[i], nums[j], need));flag = nums[j];} else {set.add(nums[j]);}}set.clear();}return result;}
}
我们最后总结一下这道题:
这道题在沿用了我们前面两数之和的思想之后,会存在一个去重问题:
- 外层重复:也就是当外层循环固定的数字和上一次相同时此次循环直接跳过
- 内层重复:也就是当内层的两数相加满足之后,内层的下一次循环还是相同的数。这个时候我们可以在每次三数之和满足条件之后,将内层此次的值记录一下,相邻的下一次循环与此次的值一样就跳过此次内循环。
当然此题也可以使用双指针法来做,逻辑上更为简单,代码在此处不多做赘述。
四数之和
题目链接:18. 四数之和
这题使用双指针法进行解题:
- 将数组进行排序
- 首先使用两层的嵌套循环,固定两个数
- 然后再使用双指针left、right确定最后两个数
- 将四个数字相加
- 如果大于目标,right指针左移
- 如果小于目标,left指针右移
- 如果达到目标,left指针右移,right指针左移
class Solution {public List<List<Integer>> fourSum(int[] nums, int target) {Arrays.sort(nums);List<List<Integer>> result = new ArrayList<>();for(int i = 0;i < nums.length;i++) {for(int j = i + 1;j < nums.length;j++) {int left = j + 1;int right = nums.length - 1;while(left < right && left < nums.length) {int sum = nums[i] + nums[j] + nums[left] + nums[right];if(sum > target) right--;else if(sum < target) left++;else {result.add(Arrays.asList(nums[i], nums[j], nums[left++],nums[right--]));}}}}return result;}
}
接下来进行去重:
发生这种情况就是因为外层的双层循环固定的两个数字重复,我们添加去重的代码:
class Solution {public List<List<Integer>> fourSum(int[] nums, int target) {Arrays.sort(nums);List<List<Integer>> result = new ArrayList<>();for(int i = 0;i < nums.length;i++) {if (i > 0 && nums[i] == nums[i - 1]) continue;for(int j = i + 1;j < nums.length;j++) {if (j > i + 1 && nums[j] == nums[j - 1]) continue;int left = j + 1;int right = nums.length - 1;while(left < right && left < nums.length) {int sum = nums[i] + nums[j] + nums[left] + nums[right];if(sum > target) right--;else if(sum < target) left++;else {result.add(Arrays.asList(nums[i], nums[j], nums[left++],nums[right--]));}}}}return result;}
}
这个去重的逻辑就是:
if (i > 0 && nums[i] == nums[i - 1]) continue;
因为数组是按大小排序的,如果第一个固定的数不变,那么其他三个数不管怎么样,都只会与target相等(这个情况已经存在需要去重)或者比target大,所以这个循环可以直接跳过if (j > i + 1 && nums[j] == nums[j - 1]) continue;
逻辑类似
执行之后有一个测试样例还是没过:
造成这个情况的原因是因为,左右指针同时内缩的时候如果元素不变也会发生重复,我们继续往里面添加去重逻辑:
class Solution {public List<List<Integer>> fourSum(int[] nums, int target) {Arrays.sort(nums);List<List<Integer>> result = new ArrayList<>();for(int i = 0;i < nums.length;i++) {if (i > 0 && nums[i] == nums[i - 1]) continue;for(int j = i + 1;j < nums.length;j++) {if (j > i + 1 && nums[j] == nums[j - 1]) continue;int left = j + 1;int right = nums.length - 1;while(left < right && left < nums.length) {long sum = (long)nums[i] + nums[j] + nums[left] + nums[right];if(sum > target) right--;else if(sum < target) left++;else {result.add(Arrays.asList(nums[i], nums[j], nums[left],nums[right]));while (right > left && nums[right] == nums[right - 1]) right--;while (right > left && nums[left] == nums[left + 1]) left++;left++;right--;}}}}return result;}
}
注意:测试用例中有一个例子考察的是四数相加的范围超出了int的最大表达值,所以四数相加的和sum要使用long来存储