两数之和
思路过程
题目解析:
- 1.从数组里面找出两个整数,使得和为target
- 要返回的是索引,所以还得知道位置
- 这里应该是已知数量的,所以是不是可以先利用target-数组的数,然后再看数组里面是否包含即可
思路:
利用map存储数组的值和索引,需要的去返回即可
代码
//初始版本
public int[] twoSum(int[] nums, int target) {
//key 为数组的值 value为索引
// 这里先去
HashMap<Integer,Integer> map=new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i],i);
}
for (int i = 0; i < nums.length; i++) {
//这里去求得所要的值
int num=target-nums[i];
//如果为true 则证明找到了
//因为不能重复,所以这里利用索引不能一样的原则去判断
if(map.containsKey(num)&&i!=map.get(num)){
return new int[]{i, map.get(num)};
}
}
return null;
}
更为快捷的方法
这里不用先去遍历nums将数先存进map,因为后面会逐渐存起来,而且后面找前面也是一样的
public int[] twoSum_2(int[] nums, int target) {
//key 为数组的值 value为索引
HashMap<Integer,Integer> map=new HashMap<>();
for (int i = 0; i < nums.length; i++) {
//这里去求得所要的值
int num=target-nums[i];
//如果为true 则证明找到了
//因为不能重复,所以这里利用索引不能一样的原则去判断
if(map.containsKey(num)&&i!=map.get(num)){
return new int[]{i, map.get(num)};
}
//todo:如果前面的找不到所要的值,这里先去存储起来即可
//nums = [2,7,11,15], target = 9
// 原因:这里是先去得到2,然后map是空的,因为9-2=7,这里的7确实nums有,但是map没有,所以这里是进行下一个循环,但是没有影响
// 因为这里的9-7=2,是一样的,所以不用担心要找的数还没有存进map导致错误的问题,因为遍历到后面会去找前面的数,结果是一样的
map.put(nums[i],i);
}
return null;
}
注意点
不用先全部存进map
//如果前面的找不到所要的值,这里先去存储起来即可
//nums = [2,7,11,15], target = 9
// 原因:这里是先去得到2,然后map是空的,因为9-2=7,这里的7确实nums有,但是map没有,所以这里是进行下一个循环,但是不影响结果
// 因为这里的9-7=2,是一样的,所以不用担心要找的数还没有存进map导致错误的问题,因为遍历到后面会去找前面的数,结果是一样的
map.put(nums[i],i);
四数之和
思路
1.去两两组合,找到那个和为0的,但是这个的结果是n^4,不行 复杂度太高
==>改进:在相加的时候,比如有四个组合:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
那么nums1和nums2去两两组合后,再和nums3和num4两两组合后,再将两个结果去组合的效果是和四个去两两组合的结果是一样的
不同的是:第一种的复杂度是n2,第二种是n4,所以采用第一种,两两组合之后,再去利用组合的结果去计算最终的组合数
代码
/**
*题目解析:
* 1.给了四个数组,然后问有多少种组合(i,j,k,l) 让nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
* 返回组合数量
*/
/**
*在处理相加/相减问题中,注意: 比如有四个数 n1,n2,n3,n4,;n1和n2去组合后,再和n3、n4去组合,结果和n1、n2、n3、n4组合的结果是一样的,但是第一种的复杂度低
* 第一种的时间复杂度是n^2,第二种是n^3,当然,也可以是n1,然后n2,n3,n4去组合,但这种的时间复杂度是n^3,不如两两组合
*/
//先计算组合1和组合2,再去利用value相乘
public static int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
//这里的key保存相加的结果,value保存出现的次数,因为key不能重复
HashMap<Integer,Integer> map12=new HashMap<>();
HashMap<Integer,Integer> map34=new HashMap<>();
//四个数组的长度是一样的
//这里去遍历nums1和nums2的组合,去得到他们的相加之后的结果
for (int i = 0; i < nums1.length; i++) {
for (int j = 0; j < nums2.length; j++) {
int sum12=nums1[i]+nums2[j];
//判断是否出现过
//这里去得到数组1、2的结果
map12.put(sum12, map12.getOrDefault(sum12,0)+1);
//todo:1.如果这里采用先去计算map34,这里去得到0-数组3、4的结果,因为这里要根据这个得出的结果去map12找
int sum34=0-(nums3[i]+nums4[j]);
map34.put(sum34, map34.getOrDefault(sum34,0)+1);
}
}
//遍历map12,去寻找map34有没有对应的【因为map34是利用0-得来的】
int count=0;
for (Map.Entry<Integer, Integer> index : map12.entrySet()) {
if(map34.containsKey(index.getKey())){
//todo:2.那么这里应该利用index.getValue()*map34.get(index.getKey()) 因为得利用map12和map34的结果
//比如:map12中有0的个数是2,map34中0个value也是2,那么最终的组合就是4,而不是2,所以这里得利用两个的value*
//todo:注意这里不是index.getValue()*map34.get(index.getKey()),组合嘛,肯定是用*
count=count+index.getValue()*map34.get(index.getKey());
}
}
return count;
}
//先计算组合1,再在组合3的过程中加上12的value
public static int fourSumCount_2(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
//这里的key保存相加的结果,value保存出现的次数,因为key不能重复
HashMap<Integer,Integer> map12=new HashMap<>();
HashMap<Integer,Integer> map34=new HashMap<>();
//四个数组的长度是一样的
//这里去遍历nums1和nums2的组合,去得到他们的相加之后的结果
for (int i = 0; i < nums1.length; i++) {
for (int j = 0; j < nums2.length; j++) {
int sum12=nums1[i]+nums2[j];
//判断是否出现过
//这里去得到数组1、2的结果
map12.put(sum12, map12.getOrDefault(sum12,0)+1);
}
}
int count=0;
//因为上面没有顺便计算map34的组合,下面计算,在计算时,顺便把对应的计数加上去了
//比如:现在已知的是map12的0是2
//然后这里:在遍历时,从num34中得到结果0,那么就去寻找map12中是否有结果为0的,如果有,则去+1,然后第二次从num34中得到0,再去寻找map12中结果为0的,再加上该次数,从而达到相乘的结果!
for (int i = 0; i < nums3.length; i++) {
for (int j = 0; j < nums4.length; j++) {
int sum=0-nums3[i]-nums4[j];
count=count+map12.getOrDefault(sum,0);
}
}
return count;
}
注意点总结
1.注意:组合与组合之间的计数,应该是乘,而不是加
比如:组合1中有0的个数为3,组合2中有0的个数为4,那么对应的总组合数为3*4,这里要得到最终的组合数得利用到两个的value,然后去相乘
2.要得到相加的结果为某个数时,
比如:num1+num2+num3+num4=m,那么一般步骤是:
0.定义一个哈希(map/set):map(定义key和value存放的意义,比如一个是sum,一个是次数)
1.先去正常计算加数:
sum1=num1+num2
【sum1=1+2=3】2.在计算另一时,是利用:
sum2=m-num3-num4
去存放结果【sum2=0-(-1)-(-2)=3】目的:1.可以直接利用map的特性:判断某个值是否在map里面,因为sum2是利用m去减后得到的,那么sum2的结果,如果与sum1相等的话,那么就证明了此
num1+num2+num3+num4=m
;【1+2+(-2)+(-3)=0】3.在计算最终的次数时,其实是利用相乘的意义。
两种过程
一、先去计算组合1(num1、num2)和组合2(num3、num4)的结果和次数value
那么在算最终的次数时,应该是count=count+map12(key)*map34(key);应该是这两个key对应的次数相乘,而不是相加
//遍历map12,去寻找map34有没有对应的【因为map34是利用0-得来的】 int count=0; for (Map.Entry<Integer, Integer> index : map12.entrySet()) { if(map34.containsKey(index.getKey())){ //todo:2.那么这里应该利用index.getValue()*map34.get(index.getKey()) 因为得利用map12和map34的结果 //比如:map12中有0的个数是2,map34中0个value也是2,那么最终的组合就是4,而不是2,所以这里得利用两个的value* //todo:注意这里不是index.getValue()*map34.get(index.getKey()),组合嘛,肯定是用* count=count+index.getValue()*map34.get(index.getKey()); } }
二、先计算组合1,然后在计算组合2时,逐渐+上count
这里的map12就是组合12的结果和的集合
然后这里去遍历num3和num4,其实在这个过程中就体现了次数相乘的过程
比如:map12(2)=3,即组合12中结果为2的次数有3个【然后我们的map34(2)=3,即也有3个】
那么在下面如何体现呢?其实这里就是遍历得到了三次3,然后根据
count+map12.getOrDefault(sum,0)
加了三次,就相当于3*3int count=0; //因为上面没有顺便计算map34的组合,下面计算,在计算时,顺便把对应的计数加上去了 //比如:现在已知的是map12的0是2 //然后这里:在遍历时,从num34中得到结果0,那么就去寻找map12中是否有结果为0的,如果有,则去+1,然后第二次从num34中得到0,再去寻找map12中结果为0的,再加上该次数,从而达到相乘的结果! for (int i = 0; i < nums3.length; i++) { for (int j = 0; j < nums4.length; j++) { int sum=0-nums3[i]-nums4[j]; count=count+map12.getOrDefault(sum,0); } }
4、当Map集合中有这个key时,就使用这个key对应的value值,如果没有就使用默认值defaultValue;
hashmap.getOrDefault(key,defaultValue);
map12.put(sum12, map12.getOrDefault(sum12,0)+1);
// 如果没有,那么就是put(sum12,0+1)
// 如果有,那么就是put(sum12,value+1)
赎金信
思路
/**
*思路过程:
* 题目解析:给两个字符串,然后判断ransomNote字符串能否由magazine构成,且这里ma的字母不能重复,所以这里得用map去记录次数
* 思路:将ma的字母记录在map,key为字母,value为次数,然后在遍历ran的时候,判断map中是否有且次数不为0的
*/
代码
public boolean canConstruct(String ransomNote, String magazine) {
HashMap<Character,Integer> map=new HashMap<>();
//这里得到了ma字符串的字母记录
for (int i = 0; i < magazine.length(); i++) {
char a=magazine.charAt(i);
map.put(a,map.getOrDefault(a,0)+1);
}
//下面去遍历ran的
for (int i = 0; i < ransomNote.length(); i++) {
char a=ransomNote.charAt(i);
//这里去判断是否存在,如果存在且次数不为0,则代表可以将这个构成ran的一部分
if(map.containsKey(a)&&map.get(a)!=0){
//将次数减1,避免重复,继续判断下一个
map.put(a,map.get(a)-1);
}else {
//如果有一个不一样,就代表不能构成,则返回false
return false;
}
}
return true;
}
注意点
用哈希函数,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!
三数之和
思路
目标是
- 题目解析:给一个整数数组,要求根据数组里面的不同索引的值得到一个三元组,使得相加为0
- 要求:1. i、j、k不能相等
-
- 不重复
-
- 返回三元组 不是返回次数了
那我们就先对数组进行排序【方便后续的指针根据大小去移动】,然后可以定义i,left,right去
先让i从0-nums.length去赋值,然后在每次的情况下,left和right去移动
移动逻辑:
定义i先固定在0,然后left和right在左右,然后将这三个对应的值相加,如果>0,则证明太大了,right-1,如果<0 则证明太小了,left+1,然后left和right移动完后,再去i++ ,下一次循环
去重逻辑:
因为这里的三元组和i、j、k都不能重复,所以这里对i、left、right都得进行去重操作
去重一般是:if (num[i]==num[i+1]) i++; continue;
即 两个相等时则进行去重,但是由于这里在进行判断时,num[i]还没有进行计算,所以无法确定该数是否应该为正确结果的一部分,所以这里先不能进行去重,因为还没有开始,所以这里应该是 if (num[i] ==num[i-1]) i++; continue;
即应该是i-1去判断。
因为当nums[i] =-1时,这个循环的过程已经将第一个数为-1的结果已经遍历完了,所以第二次当a还是-1时,就不用循环了,不然就造成重复了,所以这里使用去重
然后left和right因为是先计算后去重 所以仍然是与后一个进行判断,如果一样则去重
*思路过程:
* 题目解析:给一个整数数组,要求根据数组里面的不同索引的值得到一个三元组,使得相加为0
* 要求:1. i、j、k不能相等
* 2. 不重复
* 3. 返回三元组 不是返回次数了
* 思路:
* 1.哈希法:先遍历数组nums,得到一个和map集合,key为sum,value为索引,该数组是由nums数组中两两相加组成,然后再去相加另外一个?
* 不对,不能重复:用map存?
* 2.双指针法:感觉有去重的时候,用双指针法比较好用
* 如果要进行双指针法的排序相关,要对nums先进行从小到大排序,方便进行移动指针
* 思想:定义i先固定在0,然后left和right在左右,然后将这三个对应的值相加,如果>0,则证明太大了,right-1,如果<0 则证明太小了,left+1
* 去重逻辑:因为这里的三元组不仅仅i、j、k不能重复,他的对应的值也不能重复,所以这里注意
* [-1,0,1,2,-1,-4]
* 排完序后:[-4,-1,-1,0,1,2]
* 要去重其实很简单,比如刚开始:i是-1(索引为1),left是-1(索引为2),right是2,那么因为<0 所以是left移动,直到移动到0,此时[-1,0,1]是一个三元组
* 然后i++
* 这里的i又是-1(素引为3),那么因为这个值和num[i-1]是相同的【因为已经是排完序了,所以相同的值一定是在一起的】,然后left和right都是在后面
* 就意味着因为i又是-1,那么后面left和right在移动的过程中,如果有sum=0的,最后得到的值一定也是和前一个是三元组相同,即又是[-1,0,1] 而我们的三元组要不同的
* 所以这里去重的逻辑是:if (num[i]==num[i-1]) i++; continue; 直接开启下一个循环
* left和right的去重逻辑和这个一样
* 注意这里不是num[i]==num[i+1] 去比较 因为如果是这样的话,就不是去重的逻辑了 而是不允许三元组出现重复的元素了 但这里是允许的,只是要求三元组不能重复
*如:[0,0,0] 这里是一个正确的结果集,但如果是if (num[i]==num[i+1]) i++; continue; 那么就会导致失去这个正确的结果
* todo:本质上是因为if (num[i]==num[i-1]) i++; continue; 这个能去跳过循环是因为 排序后的 比如[0,-1,-1,1] 如果i的索引是1时,那么此时就得到了正确的结果[0,-1,1]
* 即前面一个是根据i=-1时,已经得到了一个正确的结果集了,这里是对第一个元素a去重【如果让if (num[i]==num[i+1]) i++; continue; 去进行去重 那么就会让比如:i为索引1时,因为跟索引2相同,所以跳过,失去了正确解,所以应该是跟之前的num[i-1]去比较】
* 并且这里并不会导致a=-1时的结果只剩下一个:[-1,0,1]
* 因为后面的left和right是while,他会将当第一个元素为-1的所有符合的结果都存起来 即:[-1,0,1] 和 [-1,-1,2]都存起来 所以就不需要第二个的-1了
*/
//得到结果的逻辑的注意事项:
// 排序后+要得到对应的值,不只是要结果(因为双指针可以得到对应的索引==》得到对应的结果)+不重复=双指针【因为left和right的一般结束条件是:left<right 所以不重复很重要
代码
public static List<List<Integer>> threeSum(int[] nums) {
//先排序咯,从小到大排序
Arrays.sort(nums);
List<List<Integer>> res=new ArrayList<>();
int i=0;
int left=1;
int right=nums.length-1;
// 要找到[a,b,c] 这个三元组 现在 a是i、b是left、c是right
while (i<nums.length){
if(nums[i]>0){
return res;
}
// 对a去重
if(i>0&&nums[i]==nums[i-1]){
i++;
left=i+1;
continue;
}
while (left<right){
int sum=nums[i]+nums[left]+nums[right];
if(sum>0){
right--;
}else if(sum<0){
left++;
}else {
// 如果=0
res.add((Arrays.asList(nums[i], nums[left], nums[right])));
// 注意这里不能让他有的话就结束循环,要是有两个呢?所以还得继续变化移动
// 对b和c去重:这里的去重是和下一位做比较 让left和right不断往中间移动即可,而不是和他的前一位:因为上面已经和num[left] 得到一次结果了 所以如果之后的left+1还是num[left]的话,不用比较 因为得到的结果是一样的 所以跳过
//todo:和a去重的区别是:a是还没发生,所以不能去重 得发生后才能去重,所以是和上一个比较 这里是已经发送【即已经对nums[left]进行处理了,所以是和下一个比较 一样即可跳过
// todo:注意这里是while 而不是if 如果是while的话 就可以连续去重了 if的话去重不干净,因为只去重一次
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
i++;
left=i+1;
right=nums.length-1;
}
return res;
}
注意点总结
1.遇到这种不是只要结果的,而是要对应的索引的值的组合的,不适合用哈希法【因为要保留索引,还要保留sum,真的很麻烦】
那么就可以用到双指针法,因为双指针是实时的,而且对于处理单个nums,双指针真的很好用
2.利用双指针的前提
数组排好序【可以自己排序:Arrays.sort(nums);】
定义双指针的循环和结束条件【一般是left<right】