代码随想录算法训练营第七天| 哈希表理论基础 ,454.四数相加II, 383. 赎金信, 15. 三数之和, 18. 四数之和
454.四数相加II
建议:本题是 使用map 巧妙解决的问题,好好体会一下 哈希法 如何提高程序执行效率,降低时间复杂度,当然使用哈希法 会提高空间复杂度,但一般来说我们都是舍空间 换时间, 工业开发也是这样。
题目链接/文章讲解/视频讲解:
看到题目的第一想法:
超时的想法:用map存储其中的一个数组,然后三层for循环求得三个整数数组的和sum,最后判断map中是否存在0-sum,如果存在result加上它的value值,如果不存在就下层循环。显然这种想法的时间复杂度为O(n^3).
重新思考后的想法:用map的key存储其中两个数组的所有和,value存储其个数,然后三层for循环求得另外两个数组的sum,最后判断map中是否存在0-sum,如果存在result加上它的value值,如果不存在就下层循环。这种想法的时间复杂度为O(n^2),减少了一个数量级。
package com.second.day7;
import java.util.HashMap;
public class FourSumCount_454 {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
HashMap<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < nums3.length; i++) {
int num3 = nums3[i];
for(int j = 0; j < nums4.length; j++) {
int num4 = nums4[j];
map.put(num3 + num4, map.getOrDefault(num3 + num4, 0) + 1);
}
}
int result = 0;
for(int i = 0; i < nums1.length; i++) {
int num1 = nums1[i];
for(int j = 0; j < nums2.length; j++) {
int target = 0 - num1 - nums2[j];
if(map.containsKey(target)) {
result += map.get(target);
}
}
}
return result;
}
}
看完代码随想录之后的想法:
思路是一样的,难怪leetcode的执行时间还挺快的。
自己实现过程中遇到哪些困难:
无,看到时间超了,能很快想出另外一种想法不错的。
383. 赎金信
建议:本题 和 242.有效的字母异位词 是一个思路 ,算是拓展题
题目链接/文章讲解:
看到题目的第一想法:
1.设置一个map,map的key表示magazine的字符,value表示字符出现的次数。
2.遍历magazine,将magazine的字符存入map中
3.遍历ransomNote,如果map中有不存在的字符,或map的字符的value小于0就返回false,直到遍历完。
package com.second.day7;
import java.util.HashMap;
public class FourSumCount_454 {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
HashMap<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < nums3.length; i++) {
int num3 = nums3[i];
for(int j = 0; j < nums4.length; j++) {
int num4 = nums4[j];
map.put(num3 + num4, map.getOrDefault(num3 + num4, 0) + 1);
}
}
int result = 0;
for(int i = 0; i < nums1.length; i++) {
int num1 = nums1[i];
for(int j = 0; j < nums2.length; j++) {
int target = 0 - num1 - nums2[j];
if(map.containsKey(target)) {
result += map.get(target);
}
}
}
return result;
}
}
看完代码随想录之后的想法:
思路是一样的,但卡哥用的是数组存储,比我直接用map的空间少,时间更快。以后涉及只有小写字母或者大写字母的,用哈希法先用数组存储。
自己实现过程中遇到哪些困难:
无
15. 三数之和
建议:本题虽然和 两数之和 很像,也能用哈希法,但用哈希法会很麻烦,双指针法才是正解,可以先看视频理解一下 双指针法的思路,文章中讲解的,没问题 哈希法很麻烦。
题目链接/文章讲解/视频讲解:
看到题目的第一想法:
1.设置一个map,map的key表示nums[i]的值,value表示出现的次数
2.用map存储其中的一个数组
3.两层for循环,分别求得nums[i]和nums[j],然后判断map中是否有0-nums[i]-nums[j]
4.如果map中存在,放入list中,为了去重,排序后放入set集合中
package com.second.day7;
import java.util.*;
public class ThreeSum_15 {
/**
* 一顿操作猛如虎,点击提交超时了。
*
* 二话不说翻题解,评论区里全人才。
*
* 反反复复终得道,再次尝试却报错。
*
* 行行检查字字改,击败用户百分五。
* @param nums
* @return
*/
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Set<List<Integer>> set = new HashSet<>();
//key存储nums[i]的值,value存储值的个数
HashMap<Integer, Integer> map = new HashMap<>();
for(int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
for(int i = 0; i < nums.length - 1; i++) {
int num1 = nums[i];
map.put(num1, map.get(num1) - 1);
for(int j = i + 1; j < nums.length; j++) {
int num2 = nums[j];
map.put(num2, map.get(num2) - 1);
int target = -num1 - num2;
List<Integer> list = new ArrayList<>();
if(map.containsKey(target) && map.get(target) > 0) {
list.add(num1);
list.add(num2);
list.add(target);
Collections.sort(list);
set.add(list);
}
map.put(num2, map.get(num2) + 1);
}
map.put(num1, map.get(num1) + 1);
}
for(List<Integer> lists : set) {
result.add(lists);
}
return result;
}
}
看完代码随想录之后的想法:
排序后去重的效率是很低的,然后我的时间复杂度就很高,Colletion.sort()用的归并排序或者快速排序时间复杂度为O(nlogn),然后加两层for循环时间复杂度就到了O(n^3logn).
卡哥有去重的哈希法:先对数组进行排序,两层for循环,然后每层for循环分别去重nums[i],nums[j],在第一层for循环设置一个set用来存储nums[k],如果set中存在target = nums[i] - nums[j],则符合题目要求,放入result中,并对nums[k]去重。剪枝后的时间复杂度是O(n^2)。
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for(int i = 0; i < nums.length; i++) {
//排序后的第一数都大于0,之后的肯定不满足三数之和为0
if(nums[i] > 0)
break;
//对a去重
if(i > 0 && nums[i] == nums[i - 1])
continue;
HashSet<Integer> set = new HashSet<>();
for(int j = i + 1; j < nums.length; j++) {
//对三元组b去重如[0, 0, 0, 0]
if(j > i + 2 && (nums[j] == nums[j - 1] && nums[j - 1] == nums[j - 2]))
continue;
int target = -nums[i] - nums[j];
if(set.contains(target)) {
res.add(Arrays.asList(nums[i], nums[j],target));
//对c去重
set.remove(target);
}
else {
set.add(nums[j]);
}
}
}
return res;
}
双指针法
1.首先对数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
2.依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。
3.如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
4.如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
先不看代码实现一下吧!
public List<List<Integer>> threeSum2(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for(int i = 0; i < nums.length; i++) {
//第一个数大于0了,就不会有满足题目要求的数了
if(nums[i] > 0)
break;
//对数a去重
if(i > 0 && nums[i] == nums[i - 1])
continue;
int left = i + 1;
int right = nums.length - 1;
while(left < right) {
int sum = nums[i] + nums[left] + nums[right];
if(sum > 0) {
right--;
}
else if(sum < 0) {
left++;
}
else {
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
while(left < right && nums[left + 1] == nums[left])
left++;
while(left < right && nums[right - 1] == nums[right])
right--;
left++;
right--;
}
}
}
return res;
}
啊,竟然一摸一样,时间复杂度进一步降低了!代码的世界真奇妙!这次的时间复杂度小于O(n^2)了。
自己实现过程中遇到哪些困难:
将数组转换为List的函数不熟悉,不会运用。Arrays.asList().
454.四数相加II
建议: 要比较一下,本题和 454.四数相加II 的区别,为什么 454.四数相加II 会简单很多,这个想明白了,对本题理解就深刻了。 本题 思路整体和 三数之和一样的,都是双指针,但写的时候 有很多小细节,需要注意,建议先看视频。
'题目链接/文章讲解/视频讲解:
看到题目的第一想法:
和之前的三数之和的想法类似
1.设置一个map,map的key表示nums[i]的值,value表示出现的次数
2.用map存储其中的一个数组
3.三层for循环,分别求得nums[i],nums[j]和nums[k],然后判断map中是否有0-nums[i]-nums[j]
4.如果map中存在,放入list中,为了去重,排序后放入set集合中
可想而知这个时间复杂度是很高的!但在leetcode上能够通过。期间我也遇到了一个整数溢出的问题。是之前三数之和没出现的。
package com.second.day7;
import java.util.*;
public class FourSum_18 {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
Set<List<Integer>> set = new HashSet<>();
//key存储nums[i]的值,value存储值的个数
HashMap<Integer, Integer> map = new HashMap<>();
for(int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
for(int i = 0; i < nums.length - 2; i++) {
map.put(nums[i], map.get(nums[i]) - 1);
for(int j = i + 1; j < nums.length - 1; j++) {
map.put(nums[j], map.get(nums[j]) - 1);
for(int k = j + 1; k < nums.length; k++) {
map.put(nums[k], map.get(nums[k]) - 1);
long num4 = (long)target - ((long)nums[i] + nums[j] + nums[k]);
if(num4 < Integer.MIN_VALUE || num4 > Integer.MAX_VALUE) {
map.put(nums[k], map.get(nums[k]) + 1);
continue;
}
List<Integer> list = new ArrayList<>();
if(map.containsKey((int)num4) && map.get((int)num4) > 0) {
list.add(nums[i]);
list.add(nums[j]);
list.add(nums[k]);
list.add((int)num4);
Collections.sort(list);
set.add(list);
}
map.put(nums[k], map.get(nums[k]) + 1);
}
map.put(nums[j], map.get(nums[j]) + 1);
}
map.put(nums[i], map.get(nums[i]) + 1);
}
for(List<Integer> lists : set) {
result.add(lists);
}
return result;
}
public static void main(String[] args) {
int[] nums = new int[]{1, 0, -1, 0, -2, 2};
FourSum_18 demo = new FourSum_18();
demo.fourSum(nums, 0);
}
}
看完代码随想录之后的想法:
双指针法:
(1)和三树之和的双指针法思想是一致的,但在一些细节上是不同的。因为这个题目的target是不固定的,不能直接在第一次循环的时候nums[i] > target就可以不循环,因为如果target为-10,-4>-10,nums[i]后面还有负数的话就有可能满足题目要求。
(2)四数之和相对于三数之和是有两层for循环,同样也设置left和right,然后找nums[i]+nums[j]+nums[left]+nums[right]=target的情况。
(3)用双指针法,三数之和的时间复杂度从O(n ^ 3) 降到了O(n ^ 2),四数之和的时间复杂度从O(n ^ 4) 降到了O(n ^ 3).之后的五数之和,六数之和都可以用同样的方法。
先不看卡哥代码,自己先实现一下吧。
public List<List<Integer>> fourSum1(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
//对数组进行排序
Arrays.sort(nums);
for(int i = 0; i < nums.length - 3; i++) {
//剪枝
if(nums[i] > target && (nums[i] > 0 || target > 0))
break;
//去重
if(i > 0 && nums[i] == nums[i - 1])
continue;
for(int j = i + 1; j < nums.length - 2; j++) {
//剪枝
if((nums[i] + nums[j]) > target && (nums[i] + nums[j] > 0 || target > 0))
continue;
//去重
if(j > i + 1 && nums[j] == nums[j - 1])
continue;
int left = j + 1;
int right = nums.length - 1;
while(left < right) {
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]));
while(left < right && nums[left] == nums[left + 1])
left++;
while(left < right && nums[right] == nums[right - 1])
right--;
left++;
right--;
}
}
}
}
return result;
}
注意:上面的代码没有做整数溢出处理也能过得原因是剪枝过程中规避了溢出。最好还是要像卡哥一样,求sum是把他转为long
自己实现过程中遇到哪些困难:
[-1000000000,-1000000000,-1000000000,-1000000000]
294967296
整数溢出处理:
我第一做的时候是先把他转为long,然后判断这个数是否在int的范围内,没有在的话就不符合条件,跳过去。
long num4 = (long)target - ((long)nums[i] + nums[j] + nums[k]);
if(num4 < Integer.MIN_VALUE || num4 > Integer.MAX_VALUE) {
map.put(nums[k], map.get(nums[k]) + 1);
continue;
}
好吧这种方法很笨。
还是双指针法好。
今日收获,记录一下自己的学习时长:
今天的题我虽然都硬写出来了,真就是硬写出来的,时间复杂度都太高了,好吧多数之和没有想到可以用双指针法进行剪枝与去重。今天的博客写了我两天。脚踏实地的,不骗自己。
今天写了四个题:
454.四数相加II , 383. 赎金信, 15. 三数之和 ,18. 四数之和
代码:4h
博客:1.5h
想干就干,要干就干的漂亮,即使没有人为你鼓掌,至少还能够勇敢的自我欣赏。