Bootstrap

哈希02:算法进阶,详解三数之和等难度算法

两数之和

思路过程

题目解析:

  • 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);

四数之和

454. 四数相加 II - 力扣(LeetCode)

思路

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*3

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);
    }
}

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要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!

三数之和

15. 三数之和 - 力扣(LeetCode)

思路

目标是

  • 题目解析:给一个整数数组,要求根据数组里面的不同索引的值得到一个三元组,使得相加为0
  • 要求:1. i、j、k不能相等
    1. 不重复
    1. 返回三元组 不是返回次数了

那我们就先对数组进行排序【方便后续的指针根据大小去移动】,然后可以定义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】

;