【算法练习Day6】四数相加赎金信三数之和四数之和

news2024/11/27 23:39:28

在这里插入图片描述

​📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:练题
🎯长路漫漫浩浩,万事皆有期待

文章目录

    • 四数相加
    • 赎金信
    • 三数之和
    • 四数之和
  • 总结:

这一期的题我刷的心力交瘁,感觉题目很类似,但是就是做不出来,第三道卡了一会,后来自己模拟了一遍才理解了。

同时这一篇的题,也告诉我们不是所有这种类型的题,用哈希算法来解都简单,后两种用指针法做比哈希表解更好

四数相加

454. 四数相加 II - 力扣(LeetCode)
在这里插入图片描述

题目大意就是在四个数组中,找到一个四元组相加的和为0,为什么说它较为简单呢?原因有两个它并不涉及到去重,也就是说四元组中元素可以重复,只要保证它们是来自于不同的数组就可以了。

大体思路为:用一个map哈希结构来保存前两个数组的元素之和的所有可能情况,那么问题来了我们为什么要用到map而不是其他的哈希结构呢?原因很简单,四个数组中元素个数不固定,相加大小也不固定,用数组可能会存不下,不用set的原因是因为我们需要统计前两个数组和相同的答案在哈希中出现了几次,所用map更适合,为什么要统计这个,在代码给出后会进行更详细解释。

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int> umap;//key:a+b的数值,value:a+b数值出现的次数
        for(int a:nums1)// 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
        {
            for(int b:nums2)
            {
                umap[a+b]++;
            }
        }
        int count=0;// 统计a+b+c+d = 0 出现的次数
        for(int c:nums3)// 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
        {
            for(int d:nums4)
            {
                if(umap.find(0-(c+d))!=umap.end())
                {
                    count+=umap[0-(c+d)];
                }
            }
        }
        return count;
    }
};

auto是c++的独特书写语法,在看完上述代码,应该对于为什么要使用map有了一点模糊的想法,下面我给大家详细解答一下。由于该题目,要求四元组的值不同,只要四元组各答案的下标没有和其他四元组一一对应的重复就可以,如果这样讲有点抽象,我来举例说明,当后两个数和为-5的时候说明我们应该在前两个数里面找到5来相结合,这显而易见,那么我们如果找出a[1]+b[2]=-5,a[2]+b[3]=-5,a[3]+b[3]=-5诸如此类的情况呢?这些构成四元组的答案,实际上都是合法的,这就是我们为什么要存储相同的数值不同的数组下标构成的答案的原因。

时间复杂度: O(n^2)
空间复杂度: O(n^2),最坏情况下A和B的值各不相同,相加产生的数字个数为 n^2

赎金信

383. 赎金信 - 力扣(LeetCode)

在这里插入图片描述

暴力解法
第一个思路其实就是暴力枚举了,两层for循环,不断去寻找,代码如下:

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        for (int i = 0; i < magazine.length(); i++) 
        {
            for (int j = 0; j < ransomNote.length(); j++) 
            {
                // 在ransomNote中找到和magazine相同的字符
                if (magazine[i] == ransomNote[j]) 
                {
                    ransomNote.erase(ransomNote.begin() + j); // ransomNote删除这个字符
                    break;
                }
            }
        }
        // 如果ransomNote为空,则说明magazine的字符可以组成ransomNote
        if (ransomNote.length() == 0) 
        {
            return true;
        }
        return false;
    }
};

时间复杂度: O(n^2)
空间复杂度: O(1)

这道题的思路和上一期的那道有效字母异位词思路差不太多,这道题是在一个字符串里找字符看能否拼成另一个字符串,字符只能用一次,和那道题的不同之处在于,上期的那道题我们两个字符串的字母构成要求是一样的,这道题仅仅需要用一个字符串里的全部或部分字符来构成另一个字符串,这就足够了。

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int record[26] = {0};
        //add
        if (ransomNote.size() > magazine.size()) 
        {
            return false;
        }
        for (int i = 0; i < magazine.length(); i++) 
        {
            // 通过record数据记录 magazine里各个字符出现次数
            record[magazine[i]-'a'] ++;
        }
        for (int j = 0; j < ransomNote.length(); j++) 
        {
            // 遍历ransomNote,在record里对应的字符个数做--操作
            record[ransomNote[j]-'a']--;
            // 如果小于零说明ransomNote里出现的字符,magazine没有
            if(record[ransomNote[j]-'a'] < 0) 
            {
                return false;
            }
        }
        return true;
    }
};

三数之和

15. 三数之和 - 力扣(LeetCode)
在这里插入图片描述
其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,很难直接写出没有bug的代码。而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。

这道题我认为思路上还是有难度在的,重要的是去重的思路,这道题是使用指针来实现的,题目大意是在一个给定的数组内,寻找一个三元组的和来等于0,该题为什么可以应用于指针解法呢?一个重要的判断是因为,该题返回的是数组中的元素组合,元素组合相加等于0,而并非让我们返回下标,使用双指针思路,一定要注意排序,而返回数组下标则不能排序,会打乱下标顺序。

先给出代码

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        // 找出a + b + c = 0
        // a = nums[i], b = nums[left], c = nums[right]
        for (int i = 0; i < nums.size(); i++) 
        {
            // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
            if (nums[i] > 0) 
            {
                return result;
            }
            // 错误去重a方法,将会漏掉-1,-1,2 这种情况
            /*
            if (nums[i] == nums[i + 1]) {
                continue;
            }
            */
            // 正确去重a方法
            if (i > 0 && nums[i] == nums[i - 1]) 
            {
                continue;
            }
            int left = i + 1;
            int right = nums.size() - 1;
            while (right > left) 
            {
                // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
                /*
                while (right > left && nums[right] == nums[right - 1]) right--;
                while (right > left && nums[left] == nums[left + 1]) left++;
                */
                if (nums[i] + nums[left] + nums[right] > 0) right--;
                else if (nums[i] + nums[left] + nums[right] < 0) left++;
                else {
                    result.push_back(vector<int>{nums[i], nums[left], nums[right]});
                    // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
                    while (right > left && nums[right] == nums[right - 1]) right--;
                    while (right > left && nums[left] == nums[left + 1]) left++;

                    // 找到答案时,双指针同时收缩
                    right--;
                    left++;
                }
            }

        }
        return result;
    }
};

第一层循环里第一个if相当于剪枝操作,排序后的第一个数组元素如果大于0,那么说明该数组不可能有三元组相加等于0了,这是一个技巧可以帮助我们来过滤一些数组,提高运行效率。第二个if是帮助第一个循环里的i去重的,取到的重复元素假设为1,那么这个1包含了你下个1之后的所有元素组合,他们的情况都是完全一样的,所以我们并不需要重复遍历。

第二个循环中代码核心主要是排序部分,起初left指向的是i的下一个位置,而right指向的是数组的最后一个元素,如果三个数相加和大于0那么将right向前挪动一位,如果小于0,那么就将left向后挪一位,这就体现了为什么使用指针法,为什么要对数组进行排序!当前面的调整完毕后,找到一组数据将其放入结果数组中,之后就到了最关键的一步,用两个while循环来去重left和right指向的值,当left大于等于right了立马跳出。那么为什么之后我们还是需要进行left++和right- -呢?不是只有left和right不跟相邻元素相等时才跳出来的吗?我们再进行left++和right的- -操作会不会略过去一些答案呢?

试想一下以下两种不同的情境:

第一种:没有相同的元素,当我们将答案中的一个三元组存放到结果数组后,由于没有数字重复,无法通过循环来调整left和right位置那么left和right不能得到更新会使循环陷入死循环。

第二种情况:存在相同的元素,在存放一个三元组后,我们发现left或者right相邻值有重复的情况,亦或是两者都存在相邻值与之前答案的元素相同的情况,那么我们直接进入while循环,重点来了,在循环内我们是判断left和left++,right和right- -的数有没有出现重复的情况,这样看来,我们结束循环跳出来之后的left和right指向了我们重复元素的最后一次重复的地方,因为它的确不和下一个我们要判断的数字相等,所以跳了出来,如果没有left++和right- -来调整,那么我们还会遍历到之前答案的数,这也就是为什么我们一定要有left++和right- -的缘故。

那么可不可以将left++和right–放到前面呢?答案也是可以的,如果大家觉得先判断去重,再缩小范围的思路有点怪,也可以先left++和right–调整搜索范围,再对left和right进行一个去重的判断,只不过那样我们的去重逻辑有一点小变化,这时你需要将left和它的前一个作比较,right和它的后一个作比较,理由也是和上一个思路一样的,为了避免重复遍历答案,目的是直接将此时的left和right直接置到和上一个答案三元组不同的元素中。

时间复杂度: O(n)
空间复杂度: O(1)

关于 三数之和
● 对于continue和break在三数之和的区别: continue是下一个i 还存在有可能的情况, break是无论后面多少个i我们是确定不会再出现这样的情况了

● 三数之和去重为什么碰到相邻相同的元素跳过:
一个是因为之前已经判断过,不必再判断,一个是因为必须要减枝,不然会超时,还有就是题目要求不能重复,再次执行会导致重复添加数据。

● 三数之和这么去重,是怎么保证a的去重,而没有把合适的b也去掉了呢?
因为b在a后面,当a的数字确定,后面b+c的值也确定,比如序列-2,-2,4,8,16,第一次遇到某一个a=-2的时候已经把后面所有b+c=2的情况跑完了,第二次再次遇到a=-2的时候已经没有跑的意义了,更遑论有没有去掉合适的b,而且有可能第一次遇到a=-2的时候,可能正好有b=-2,c=4这种情况,但你第二次遇到a=-2,再去跑,就会发现甚至这种情况还会被漏掉。 nums[i] == nums[i-1]基本就是应对这两种情况

● 对于哈希法,b和c去重的逻辑:
对于 b 的去重,一般可以和 a 一样检查当前的 b 是否和前一个 b 相同,如果相同,则跳过当前的 b。这样可以保证每个 b 只被使用一次。但是这种方法有一个问题,就是如果数组中有连续三个或以上相同的元素,那么第一个和第二个元素都会被跳过,导致漏掉一些可能的解。例如,如果数组中有三个0,那么[0,0,0]就是一个有效的解,但是用这种方法就会被忽略。
为了解决这个问题,可以改进一下条件,只有当前的 b 和前两个 b 都相同时才跳过当前的 b。这样可以保 证至少有一个 b 被使用,并且不会出现重复。
对于 c 的去重,利用哈希集合的特性,在找到一个 c 后将其从哈希集合中删除。这样可以保证每个 c 只被使用一次且不会出现重复。

四数之和

18. 四数之和 - 力扣(LeetCode)

在这里插入图片描述

这道题的解题思路和上一道的三数之和思路十分类似,题目大意是给你一个数组,要求返回若干四元组和为target,在剪枝方面与上一题有所不同,因为上一题是总和等于0,那么排序后第一个元素大于0,直接剪枝,但是这道题是要求四元组和构成target,而target可能是负数,所以不能单纯判断其大于0,那么可不可以单纯判断nums[i]>target呢?也是不可以的,因为如果第一个元素是大于target的负数,但是第二个元素也是一个负数,那说不定就落下一个答案了。这些细节我们都需要注意,先看代码

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        for (int k = 0; k < nums.size(); k++) 
        {
            // 剪枝处理
            if (nums[k] > target && nums[k] >= 0) 
            {
            	break; // 这里使用break,统一通过最后的return返回
            }
            // 对nums[k]去重
            if (k > 0 && nums[k] == nums[k - 1]) 
            {
                continue;
            }
            for (int i = k + 1; i < nums.size(); i++) 
            {
                // 2级剪枝处理
                if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) 
                {
                    break;
                }

                // 对nums[i]去重
                if (i > k + 1 && nums[i] == nums[i - 1]) 
                {
                    continue;
                }
                int left = i + 1;
                int right = nums.size() - 1;
                while (right > left) 
                {
                    // nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出
                    if ((long) nums[k] + nums[i] + nums[left] + nums[right] > target) {
                        right--;
                    // nums[k] + nums[i] + nums[left] + nums[right] < target 会溢出
                    } 
                    else if ((long) nums[k] + nums[i] + nums[left] + nums[right]  < target) {
                        left++;
                    } 
                    else 
                    {
                        result.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
                        // 对nums[left]和nums[right]去重
                        while (right > left && nums[right] == nums[right - 1]) right--;
                        while (right > left && nums[left] == nums[left + 1]) left++;

                        // 找到答案时,双指针同时收缩
                        right--;
                        left++;
                    }
                }
            }
        }
        return result;
    }
};

代码不算短,有很多嵌套逻辑,但是整体和三数之和是一样的,只不过四数之和多了一层循环仅此而已,同样的前两层可以做适当的剪枝操作,每一层都需要进行判重,防止四元组重复,

需要注意的点在于,剪枝操作和三数之和有所不同,最后一层判断时,要加上强制类型转换为long类型,以防止数据和过大,int放不下。

关于 四数之和
● 四数之和里针对剪枝的if语句进行了讨论,在第二层剪枝中使用if(nums[k] + nums[i] > target && nums[i] > 0)剪枝范围会更大(原本是if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0))

● 剪枝条件必须要加,leetcode上有一组[100000, 100000, 100000, 100000]的输入,target为-2^31 ,这组数据应该输出空数组,但是这四个数相加会溢出等于-2^31次方,然后输出[100000, 100000, 100000, 100000]的四元组,这是错误的,所以剪枝条件必须要加,判断 nums[k] + nums[i] 是正数又大于target后,可以直接return,因为排序后正数往后都是正数了,相加只会越来越大,没理由能找到满足target的四元组。

总结:

今天的题都有一定难度,相关的思想需要多复习回顾。接下来,我们继续进行算法练习·。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1042148.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

驱动开发---基于gpio子系统编写LED灯的驱动

一、GPIO子系统相关API 1.解析GPIO相关的设备树节点 struct device_node *of_find_node_by_path(const char *path) 功能&#xff1a;根据设备树节点路径解析设备树节点信息 参数&#xff1a; path&#xff1a;设备树所在的节点路径 /mynode0X12345678 返回值&#xff1a;成…

【Aurora 8B/10B IP(1)--初步了解】

Aurora 8B/10B IP(1)–初步了解 1 Aurora 8b/10b IP的基本状态: •通用数据通道吞吐量范围从480 Mb/s到84.48 Gb/s •支持多达16个连续粘合7GTX/GTH系列、UltraScale™ GTH或UltraScale+™ GTH收发器和4绑定GTP收发器 •Aurora 8B/10B协议规范v2.3顺从的 •资源成本低(请参…

环保电商:可持续发展在跨境电子商务中的崭露头角

近年来&#xff0c;环保意识的崛起和可持续发展的重要性日益凸显&#xff0c;已成为全球关注的焦点。在这个背景下&#xff0c;电子商务行业也逐渐加入了可持续发展的浪潮&#xff0c;形成了新的商业机会。 跨境电子商务领域&#xff0c;一股环保电商的潮流正崭露头角&#xf…

推荐一个AI人工智能技术网站(一键收藏,应有尽有)

1、Mental AI MentalAI&#xff08;https://ai.ciyundata.com/&#xff09;是一种基于星火大模型和文心大模型的知识增强大语言模型&#xff0c;专注于自然语言处理&#xff08;NLP&#xff09;领域的技术研发。 它具备强大的语义理解和生成能力&#xff0c;能够处理各种复杂的…

SRM系统一键查询:简化采购流程

一、SRM系统一键查询的意义和功能 1. 统一数据源&#xff1a;SRM系统将企业的供应商信息、采购订单、交易记录等数据整合到一个统一的平台&#xff0c;方便用户进行查询和分析。 2. 快速访问供应商信息&#xff1a;一键查询功能使用户能够快速访问和查找特定供应商的详细信息…

如何在居家办公、咖啡厅办公和联合办公空间办公中抉择?

在选择居家办公、咖啡厅办公和联合办公空间办公时&#xff0c;需要考虑以下因素&#xff1a; 1. 工作效率&#xff1a;居家办公可以提供一个安静、无干扰的环境&#xff0c;这有时可以提高工作效率。然而&#xff0c;如果家庭环境不够理想&#xff0c;或者需要与同事进行面对面…

讲讲项目里的状态存储器vuex

前言 在一个企业级的应用里&#xff0c;状态存储器起着举足轻重的作用。与我们日常的练手项目不同&#xff0c;企业级项目的vuex更专注更集中更便捷。 简单回顾 让我们简单回顾一下vuex这个插件的用法。 Vuex 的状态存储是响应式的。 当 Vue 组件从 store 中读取状态的时候&…

ShardingSphere分库分表(二):基础介绍

1、简介 Apache ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈&#xff0c;它由 JDBC、Proxy 和 Sidecar&#xff08;规划中&#xff09;这 3 款相互独立&#xff0c;却又能够混合部署配合使用的产品组成。 它们均提供标准化的数据分片、分布式事务和数…

文件包含漏洞原理刨析

文件包含漏洞 开发人员通常会把可重复使用的函数写到单个文件中&#xff0c;在使用某些函数时&#xff0c;直接调用此文件&#xff0c;而无需再次编写&#xff0c;这种调用文件的过程一般被称为包含。 为了使代码更加灵活&#xff0c;通常会将被包含的文件设置为变量&#xf…

flutter版本dart版本对应关系

Flutter 版本架构Ref发布日期Dart 版本3.13.5x6412fccda2023/9/213.1.23.13.5 file3.13.5arm6412fccda2023/9/213.1.23.13.5 file3.13.4x64367f9ea2023/9/143.1.23.13.4 file3.13.4arm64367f9ea2023/9/143.1.23.13.4 file3.13.3arm64b0daa732023/9/143.1.13.13.3 file3.13.3x64…

希望杯、希望数学系列竞赛辨析和希望数学超1G的真题和学习资源

中国的中小学数学竞赛种类非常多&#xff0c;但是说到全国性的数学竞赛&#xff0c;影响力最大的之一就是“希望杯”&#xff0c;在2017年国家喊停学科竞赛后&#xff0c;“希望杯”逐步停止了&#xff0c;但是鉴于希望杯的巨大影响力&#xff0c;以及背后的利益纠葛&#xff0…

【网络安全】一篇文章带你了解CTF那些事儿

文章目录 一、什么是CTF&#xff1f;二、CTF需要学习那些知识&#xff1f;新书推荐适合新手自学的网络安全基础技能“蓝宝书”&#xff1a;《CTF那些事儿》内容简介读者对象专家推荐目录 一、什么是CTF&#xff1f; CTF&#xff08;Capture The Flag&#xff09;中文一般译作夺…

BinDiff:二进制文件的开源比较工具

BinDiff 是一个二进制文件比较工具&#xff0c;可以快速查找反汇编代码中的差异和相似之处&#xff0c;它已开源。 使用 BinDiff&#xff0c;您可以识别并隔离供应商提供的补丁中漏洞的修复。 您还可以在同一二进制文件的多个版本的反汇编之间移植符号和注释&#xff0c;或使…

ORACLE 内存结构之系统全局区(SGA)

每个 Oracle 数据库实例都会在内存中分配一个很大的内存结构&#xff0c; 称为系统全局区(System Global Area), 这是一个大型的共享内存结构,每个Oracle进程都会访问它。 在Linux/Unix操作系统上,SGA是一个物理实体&#xff0c;使用操作系统命令能“看到它”。 它被操作系…

计算机类软件方向适合参加的比赛

前言 博主是一名计算机专业的大三学生&#xff0c;在校时候参加了很多比赛和训练营&#xff0c;现在给大家博主参加过的几个的比赛&#xff0c;希望能给大一大二的学生提供一点建议。 正文 最近也有比赛的&#xff0c;我会从时间线上来给大家推荐一些比赛&#xff0c;并且给…

NLP中token总结

Token 可以被理解为文本中的最小单位。在英文中&#xff0c;一个 token 可以是一个单词&#xff0c;也可以是一个标点符号。在中文中&#xff0c;通常以字或词作为 token。ChatGPT 将输入文本拆分成一个个 token&#xff0c;使模型能够对其进行处理和理解 在自然语言处理&#…

virtualbox安装linux虚拟机访问互联网(外网)的方法

virtualbox安装linux虚拟机访问互联网&#xff08;外网&#xff09;的方法 设置方法效果图 设置方法 效果图

【C++11】万能引用与完美转发

文章目录 1. 模板中的&&—万能引用2. 完美转发及其应用场景3. 用到的代码3.1 string.h3.2 list.h3.3 test.cpp 1. 模板中的&&—万能引用 首先我们来看这样一段代码&#xff1a; 这里有4个函数&#xff0c;我们很容易能看出来它们是一个重载的关系 然后我们给这…

Windows下libmodbus 支持upd库的编译与Qt里的调用

一. libmodbus 支持udp版库下载 https://github.com/systemmonkey42/libmodbus/tree/udp_support 二. MSYS2编译工具安装 &#xff08;1&#xff09;下载MSYS2并安装 下载地址&#xff1a;https://www.msys2.org/ 双击该exe&#xff0c;安装msys2 (2) 安装编译所需库 在软件安…

js惰性函数 ----如何让函数执行之后只执行函数某一部分

看下面这份ts代码 实现的效果也很简单,就是将一份文本,复制到剪切板上,未了兼容更多的浏览器(没错说的就是你>ie !),做了一个兼容性判断, 当浏览器支持navigator.clipboard这个api时,就直接调用这个api将文本复制到剪切板中, 如果不支持这个api的话,就执行else里面的代码,这…