代码随想录算法训练营第七天|454. 四数相加 II、383. 赎金信、15. 三数之和、18. 四数之和

454. 四数相加 II

这道题暴力解法就是四个for循环嵌套,时间复杂度为n4

卡哥的讲解中,核心思路我觉得可以这么理解:

  1. 把A、B分为一组,C、D分为一组,创建map1、map2
  2. map1每组键值对的含义为(A、B中的元素能组合出来的某个数字,这个数字共有多少个下标组合),比如说如果map1中的某一条为(2,3),说明A、B中有3组下标的组合能够新加得到2。map2的含义类似。
  3. 设置一个sum,记录组合的总数
  4. 遍历map1,每拿到一个键key1,就查map2的键中有没有0-key1,如果有,key1对应的值v1就和key2对应的值v2相乘,再累加到sum中,表示ABCD四个数组中,共有v1*v2种下标组合,能够以key1+key2=0的形式达到A[i] + B[j] + C[k] + D[l] = 0

但是这么写的话就得两次for遍历A、B+两次for遍历C、D+额外遍历一次map1/map2,而卡哥这么写就不用额外遍历一次map

为什么要2+2分组?因为1+3分组的话时间复杂度就是n3

为什么想到用哈希表呢?
我感觉这道题有种根据目标,拿着A去问B有没有的那种感觉,所以可以用哈希表。
不过卡哥说这道题是哈希表经典题目,那就记住吧。

为什么能用哈希表?
因为不需要去重。当然要去重的题目哈希表也可以做,就是比较麻烦

为什么要用Map?
因为这道题不仅要记录“有没有”,还要记录“有多少”。Set只有一个位置,只能记录“有没有”

public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
    HashMap<Integer, Integer> map1 = new HashMap<>();
    int len = nums1.length;
    int count = 0;
    for (int i = 0; i < len; i++) {
        for (int j = 0; j < len; j++) {
            int sum1 = nums1[i] + nums2[j];
            map1.put(sum1, map1.getOrDefault(sum1, 0) + 1);
        }
    }

    for (int i = 0; i < len; i++) {
        for (int j = 0; j < len; j++) {
            int sum2 = nums3[i] + nums4[j];
            int target = -sum2;
            if (map1.containsKey(target)) {
                count += map1.get(target);
            }
        }

    }
    return count;
}

383. 赎金信

如果只有小写字母或者只有大写字母,那么就可以用数组做哈希表

思路方面没啥

public boolean canConstruct(String ransomNote, String magazine) {
    if (ransomNote.length() > magazine.length()) return false;
    int[] letters = new int[26];
    // 计算magazine中出现了哪些字母,每个字母出现了多少次;
    for (int i = 0; i < magazine.length(); i++) {
        letters[magazine.charAt(i) - 'a']++;
    }
    // ransomNote开始消耗字母
    for (int i = 0; i < ransomNote.length(); i++) {
        letters[ransomNote.charAt(i) - 'a']--;
    }
    // 遍历letters,如果有<0的说明就不行
    for (int i = 0; i < letters.length; i++) {
        if (letters[i] < 0) return false
            }
    return true;
}

15. 三数之和

思路方面我觉得记住就好,把这道题当做母题

暴力解法是三个for循环,卡哥的解法是用双指针把两个for循环改成一次循环遍历,我觉得挺有意思的

由于这道题求的具体的三元组,不关心位置,因此可以把数组排序

我觉得这道题最难的地方在于想清楚去重的逻辑:

  1. 对于a,要朝后看,因为三元组中可能出现(x,x,y)这种形式
  2. 对于b,c,要朝前看,因为当我在找b、c的时候,代表a已经确定,此时要找的b+c就是一个确定值。假如说我找到了b0+c0满足要求,那我就不可能在a不变的情况下再找到一组b0+c1或者b1+c0也满足要求,因此要朝前去重。 去重的时候要注意,一定要先找到,再考虑去重

双指针的面对面遍历的时候要注意:

  1. 有没有超过数组边界
  2. 有没有超过对面的指针
public List<List<Integer>> threeSum(int[] nums) {
    List<List<Integer>> ans = new ArrayList<>();
    // 对数组排序
    Arrays.sort(nums);
    // 剪枝:如果第一个数就>0,那么直接返回null
    if (nums[0] > 0) return ans;
    // a开始遍历
    for (int i = 0; i < nums.length - 2; i++) {
        // 对a去重
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        int left = i + 1;
        int right = nums.length - 1;
        // b,c开始遍历
        while (left < right) {
            if (nums[i] + nums[left] + nums[right] == 0) { //找到一组满足要求的
                // 创建三运组
                List<Integer> ansUnit = Arrays.asList(nums[i], nums[left], nums[right]);
                // 添加进答案
                ans.add(ansUnit);

                // 对b去重
                while (left < right && nums[left] == nums[left + 1]) left++;
                // 对c去重
                while (left < right && nums[right] == nums[right - 1]) right--;

                //寻找下一组
                left++;
                right--;
            } else if (nums[i] + nums[left] + nums[right] > 0) {
                right--;
            } else {
                left++;
            }
        }
    }
    return ans;
}

然后我思考了一下,什么时候去重的时候要朝前看,什么时候去重的时候要朝后看呢?我希望有一个大一统的理论。

说一下我思考的结果,我首先要明确向前看和向后看去重的含义是什么。
x向前看进行去重,代表的就是要求结果集中每个结果单位中x的对应位置上,在当前的限制条件下,不能是同一个元素。向前看进行去重的方式是先找到,再剔除。
x向后看进行去重,代表的就是要求结果集中每个结果单位中x的对应位置上,在当前的限制条件下,可以是同一个元素。向后看进行去重的方式是先判断,然后决定要不要contine

那么思考这道题。

首先思考a,a代表三元组中第一个位置,选择什么去重方式呢?

如果在for循环体开头进行去重,那么此时a没有限制,三元组中的第一个位置的数可以重复,比如(-2,0,2)和(-2,1,1),因此是向后看,就是先判断重不重,再判断要不要continue。

如果在for循环结尾进行去重,那么此时的条件为三元组中第一个数为x的组合已经全部被找到,那么此时三元组的第一个位置的数不能重复,因此要向前看。也就是下面这个版本

public List<List<Integer>> threeSum2(int[] nums) {
    List<List<Integer>> ans = new ArrayList<>();
    // 对数组排序
    Arrays.sort(nums);
    // 剪枝:如果第一个数就>0,那么直接返回null
    if (nums[0] > 0) return ans;
    // a开始遍历
    for (int i = 0; i < nums.length - 2; i++) {
        // 对a去重
        //            if (i > 0 && nums[i] == nums[i - 1]) continue;
        int left = i + 1;
        int right = nums.length - 1;
        // b,c开始遍历
        while (left < right) {
            if (nums[i] + nums[left] + nums[right] == 0) { //找到一组满足要求的
                // 创建三运组
                List<Integer> ansUnit = Arrays.asList(nums[i], nums[left], nums[right]);
                // 添加进答案
                ans.add(ansUnit);

                // 对b去重
                while (left < right && nums[left] == nums[left + 1]) left++;
                // 对c去重
                while (left < right && nums[right] == nums[right - 1]) right--;

                //寻找下一组
                left++;
                right--;
            } else if (nums[i] + nums[left] + nums[right] > 0) {
                right--;
            } else {
                left++;
            }
        }
        // 对a进行去重,向前看版本
        while (i < nums.length - 2 && nums[i] == nums[i+1]) i++;
    }
    return ans;
}

接下来思考b。

b的限制条件为三元组第一个位置已经确定,也就是说b+c是一个定值,那么就不可能出现多个三元组,a、b位置都相同,c不同;同理,不可能出现多个三元组a、c位置都相同,b不同。因此b、c都要用向前看,就是先找到,再剔除。

18. 四数之和

思路和三数之和差不多,关键还在于去重。

我a、b都放在循环开头去重,因此都是无限制,用向后看。

c、d和上一题的b、c没区别,你把a、b看成一个整体,那么就是上一题中一个确定的A

要用long,有点小坑

public List<List<Integer>> fourSum(int[] nums, int target) {
    List<List<Integer>> ans = new ArrayList<>();
    Arrays.sort(nums);

    //开始遍历a
    for (int i = 0; i < nums.length - 3; i++) {
        // 对a进行向后看去重
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        // 开始遍历b
        for (int j = i + 1; j < nums.length - 2; j++) {
            // 对b进行向后看去重
            if (j > i + 1 && nums[j] == nums[j - 1]) continue;
            // 定义c、d
            int left = j + 1;
            int right = nums.length - 1;
            // 开始遍历c、d
            while (left < right) {
                long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
                if (sum < target) {
                    left++;
                } else if (sum > target) {
                    right--;
                } else {
                    // 创建四元组
                    List<Integer> ansUnit = Arrays.asList(nums[i], nums[j], nums[left], nums[right]);
                    // 添加进答案
                    ans.add(ansUnit);
                    // 对c去重
                    while (left < right && nums[left] == nums[left + 1]) left++;
                    // 对d去重
                    while (left < right && nums[right] == nums[right - 1]) right--;
                    //寻找下一组
                    left++;
                    right--;
                }
            }
        }
    }
    return ans;
}
  • 11
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14天的训练营中,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15天的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16天的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值