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

本文详细介绍了使用哈希表和双指针解决四数相加II、赎金信、三数之和问题的方法。通过优化时间复杂度,避免暴力求解,强调了在算法设计中如何权衡时间和空间效率。双指针技巧在有序数组中尤其有效,减少了不必要的计算。
摘要由CSDN通过智能技术生成

2023年3月7日,2023年3月12日 补卡。

7 第三章 哈希表

今日任务

● 454.四数相加II

● 383. 赎金信

● 15. 三数之和

● 18. 四数之和

● 总结


454.四数相加II

【链接】(文章,视频,题目)

建议:本题是 使用map 巧妙解决的问题,好好体会一下 哈希法 如何提高程序执行效率,降低时间复杂度,当然使用哈希法 会提高空间复杂度,但一般来说我们都是舍空间 换时间, 工业开发也是这样。

题目链接/文章讲解/视频讲解:代码随想录

【第一想法与实现(困难)】

  • 暴力,四层for循环,时间复杂度O(n^4),超出时间限制

  • 为什么用哈希表,因为需要大量查询是否出现过相反数,及其(下标组合)出现次数

  • 类似两数之和,能否前两个数组之和作为key, 次数作为value,后两个数组之和作为key,次数作为value,然后存起来?这样的时间复杂度是平方

【看后想法】与我的想法基本一致

  • for冒号表达式,在不需要下表情况下很简洁,注意要写类型int,或者自动推断auto

    • for (int num : nums_vector) {}
  • 无序映射的插入,直接使用中括号即可。umap[key]++;如果key不存在,则自动创建为0,适合表达为value是key的出现次数

【实现困难】

【自写代码】

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        // 用无序映射,key是两数组各自元素和,value是次数
        // 代码简洁重构
        std::unordered_map<int, int> umap; // key是前两数组各自元素之和,value是出现次数
        for (int n1 : nums1) {
            for (int n2 : nums2) {
                umap[n1 + n2]++; // 自动创建新key
            }
        }
        int count = 0;
        for (int n3 : nums3) {
            for (int n4 : nums4) {
                if (umap.find(-n3-n4) != umap.end()) {
                    count += umap[-n3-n4];
                }
            }
        }
        return count;
    }
};

【收获与时长】半小时,umap操作熟悉,设计哈希表的值,不需要是下标,只要是次数即可


383. 赎金信

【链接】(文章,视频,题目)

建议:本题 和 242.有效的字母异位词 是一个思路 ,算是拓展题

题目链接/文章讲解:代码随想录

【第一想法与实现(困难)】都是小写字母,可以用数组。但是长度可能有1e5,会重复多次查询某字母的出现次数,考虑用无序映射。

【看后想法】

  • 暴力枚举,对于每个杂志字母,消掉可能的赎金信字母,最终如果赎金信字符串length=0。说明可以组成。关注思路与字符串操作。在第二层for中找到赎金信字母之后要break,保证不会重复删除,下表正确性

    • str.erase(str.begin() + j); // 删除str[j]

    • 字符串长度str.length()

  • 注意本意已知key只会是小写字母情况下,可以直接用数组更好,避免了map的额外空间,避免红黑树,哈希表,哈希函数额外时间

【实现困难】

【自写代码】

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        // 使用数组
        if (ransomNote.length() > magazine.length()) {
            return false;
        }
        int record[26] = {0};
        for (char c : magazine) {
            record[c - 'a']++;
        }
        for (char c : ransomNote) {
            record[c - 'a']--;
            if (record[c - 'a'] < 0) {
                return false;
            }
        }
        return true;
    }
};

【收获与时长】半小时。限定了key的小范围之后,优先考虑使用数组,节省时间


【链接】(文章,视频,题目)

15. 三数之和

建议:本题虽然和 两数之和 很像,也能用哈希法,但用哈希法会很麻烦,双指针法才是正解,可以先看视频理解一下 双指针法的思路,文章中讲解的,没问题 哈希法很麻烦。

题目链接/文章讲解/视频讲解:代码随想录

【第一想法与实现(困难)】

  • 暴力,三层for,超时,308 / 312 个通过的测试用例

  • 哈希表,类似两数之和,把i, j的和与下标存起来。但是这样可能问题是相同的和(key)对应不同的两个元素,下标

  • 即使告诉我双指针,也没想到是什么,快慢,双向?

【看后想法】

  • 哈希表,难点在于手动去重…去重逻辑比较难

  • 双指针。因为要返回的是元素,而不是下标,可以直接对数组进行排序

    • a, b, c,对应下标i, left, right。由于已经排序,在nums[i]=a固定情况下,left++右移,或者right–左移,直至相遇

    • a去重

      • 如果a>0,已经结束直接return,因为正数只会越来越大,不会=0了

      • 先收获再去重。因此i要与之前的i-1比较,而不是与之后的i+1比较

        • 我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的
    • b,c去重,left, right

      • 先收获,再去重。去重只要去掉连续出现的值即可

【实现困难】

  • 记得先排序!双指针方法一般利用题目数据的顺序特性,需要是有序数组

【自写代码】

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        // 由于返回元素三元组,而不是下标,可以先排序
        // i, left, right,双指针,两层,平方时间复杂度
        vector<vector<int>> res;
        std::sort(nums.begin(), nums.end());
        for (int i = 0; i < nums.size(); i++) {
            if (nums[i] > 0) {
                return res;
            }
            if (i > 0 && nums[i] == nums[i-1]) { // a去重,前面已经遍历过
                continue;
            }
            int left = i + 1;
            int right = nums.size() - 1;
            while (left < right) { // 三元组,不能取等号
                int sum = nums[i] + nums[left] + nums[right];
                // std::cout << "i, left, right:" << i << ',' << left << ',' << right << ",nums:" << nums[i] << ',' << nums[left] << ',' << nums[right] << std::endl;
                if (sum < 0) {
                    left++;
                } else if (sum > 0) {
                    right--;
                } else {
                    // 找到了三元组
                    std::vector<int> vec = {nums[i], nums[left], nums[right]};
                    res.emplace_back(std::move(vec));
                    // b, c去重
                    while (left < right && nums[left] == nums[left+1]) {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right-1]) {
                        right--;
                    }
                    left++;
                    right--;
                }
            }
        }
        return res;
    }
};

【收获与时长】

  • 整体感觉这题思路比较难,一个半小时。

  • 双指针方法比较巧妙。为什么能想到双指针呢?首先,暴力解法是立方时间复杂度。本题之哟啊返回元素三元组,而不需要下标。可以先自带排序,NlogN时间复杂度。这样就可以利用有序信息,根据与目标结果的大小比比较,做双指针的移动。i, left, right。

  • 哈希方法没看明白对于b, c去重的部分

  • 有一句评论比较精辟:说白了就是降维处理, 由三维降到二维, 针对任意索引i的nums[i] 求[i+1,size )范围内不重复的twoSum target = - nums[i];

    • 后面还有一个四数之和, 一样的问题, 先降到三维, 再降到二维

18. 四数之和

【链接】(文章,视频,题目)

建议: 要比较一下,本题和 454.四数相加II 的区别,为什么 454.四数相加II 会简单很多,这个想明白了,对本题理解就深刻了。 本题 思路整体和 三数之和一样的,都是双指针,但写的时候 有很多小细节,需要注意,建议先看视频。

题目链接/文章讲解/视频讲解:代码随想录

【第一想法与实现(困难)】

  • 与三数之和类似,也是不能重复,那就i, j, left, right?由于只要返回元素而不是下标,可以先排序(O(NlogN)),时间复杂度O(n^3)

  • 运行问题,int超出范围,越界

  • 想到两个for不需要都从左边开始,可以一个从左边,一个从右边,中间再用left, right,也就是i, left, right, j

  • 注意别忘了先排序,注意break条件

【看后想法】

实现方法与我想的差不多。唯一不太一样的是剪枝与二级剪枝。他使用的是判断大于0,并且要求

【实现困难】

【自写代码】

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        std::sort(nums.begin(), nums.end());
        vector<vector<int>> res;
        for (int i = 0; i < nums.size(); i++) {
            if ((long int) nums[i] * 4 > target) {
                break;
            }
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            for (int j = nums.size() - 1; j >= 0; j--) {
                if ((long int) nums[j] * 4 < target) {
                    break;
                }
                if (j < nums.size() - 1 && nums[j] == nums[j + 1]) {
                    continue;
                }
                int left = i + 1;
                int right = j - 1;
                while (left < right) {
                    long int leftover = (long int) nums[i] + nums[left] + nums[right] + nums[j] - target;
                    if (leftover < 0) {
                        left++;
                    } else if (leftover > 0) {
                        right--;
                    } else {
                        res.emplace_back(vector<int>{nums[i], nums[left], nums[right], nums[j]});
                        while (left < right && nums[left] == nums[left + 1]) {
                            left++;
                        }
                        while (left < right && nums[right] == nums[right - 1]) {
                            right--;
                        }
                        left++;
                        right--;
                    }
                }
            }
        }
        return res;
    }
};

【收获与时长】1.2小时

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值