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

文章目录

    • 454.四数相加II
      • 思路
    • 383.赎金信
      • 思路
    • 15.三数之和
      • 思路
      • 剪枝+去重
    • 18.四数之和
      • 思路
      • 剪枝+去重
      • 复习:C++中的类型转换方法
    • 总结

今天是哈希表专题的第二天

废话不多说,直接上题目

454.四数相加II

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

题目链接:454. 四数相加 II - 力扣(LeetCode)

题目说明:本题需要考虑四个数组如何组合,加和为0。只需要计算有多少种组合,而不需要写出具体的组合方式。同时,本题认为:对于数组中位置不同的元素a、b,即使a与b的值相同(a == b),a、b也被视为不同的元素,所以我们不需要“去重”,比如四个数组的元素都是n个0,则认为有不只一个组合,每个组合都是0000、0000···,区别在于不同组合中的0取自数组的不同位置(比如第一个组合的第一个0取自nums1[0],第二个组合的第一个0取自nums1[1]),这是本题与后面的一道题目 18.四数之和 的不同之处

思路

这道题目解法很巧妙,但是有迹可循:前一篇文章day6中的最后一道题 1.两数之和 思路与本题相似,我们可以参照 两数之和 中的思路

两数之和中,需要在同一个数组中找到相匹配(a+b == target)的元素a、b的下标,思路是:遍历一遍整个数组,同时创建一个map映射作为哈希表记录遍历过的元素的值和下标。每遍历到一个元素a,就从哈希表中查找是否有相匹配的元素b,即auto it = map.find(target-a)。若b在哈希表中,则返回a和b的下标,即return {i, it->second},遍历结束;否则,将元素a的 值和下标 作为键值对,添加到map中,继续遍历

我们把这个思路应用到这道题中:首先遍历数组A、B,使用map记录数组A、B的和(a+b),然后双层for循环,求数组C、D的和(c+d),在 map中查找是否存在相匹配的a+b(相匹配指的是a+b+c+d == 0),即auto it = map.find(0-(c+d))。这是一个大概的思路,我们接下来讨论一些细节问题

  • 为什么不用数组或集合作为哈希表

    数组作为哈希表乍一看可以,但是本题中a+b的值是没有大小限制的,这就戳到了数组的痛点:键的大小必须有大小限制,因此不能使用数组作为哈希表

    集合中只能存储键a+b的值,无法存储a+b出现次数的信息,因此也不能使用集合作为哈希表

  • map的键值是什么

    题目需要求出a+b+c+d == 0的组合数量,而不用列出具体的组合的方式。如果0-(c+d)在map中找到了,即it != map.end(),那么it->second应该是a+b+c+d == 0的组合数量,这个组合的数量 等于 a+b=0-(c+d)出现的次数,用下面的例子解释:

    image-20240724153341415

    若当前c=-1,d=2,那么要从map中找到a+b = 0-(c+d) = -1。map中,a+b = -1共出现了一次:a=1,b=-2,所以当c=-1,d=2时,a+b+c+d == 0这样的组合只有一个,就是a+b=-1出现的次数

    通过上述分析可知,我们使用0-(c+d)在map中寻找,需要获得a+b=0-(c+d)的出现次数,因此map的键值对为**<a+b的值val, a+b=val的出现次数>**

  • 能不能只用A构建map,然后再遍历B、C、D三个数组寻找0-(b+c+d)

    可以,但是时间复杂度是O(n3),而我们两两遍历,时间复杂度是O(n2),因此还是用A、B构建map,然后再遍历C、D更加合适

  • 整体思路

    首先遍历数组A、B,使用map记录数组A、B的和(a+b),key存放a和b两数之和,value存放a和b两数之和出现的次数。然后遍历数组C、D,求数组C、D的和(c+d),在map中查找是否存在相匹配的a+b(相匹配指的是a+b+c+d == 0),即auto it = map.find(0-(c+d))。如果找到了,即it != map.end(),那么count += it->second(count加上key对应的value),统计这个组合的数量;否则不做处理。遍历完C和D后返回count,这个count就是四个数组中a+b+c+d == 0的组合数量

代码实现

class Solution {
public:int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {// 思路就是先进行两个数组A、B的加法,得到一个哈希表,key为a+b的值,value为这个值出现的次数// 然后双层for循环,先对C、D做加法,和为c+d,然后再哈希表中查找0-(c+d),如果找到了,则这个键对应的值是a+b+c+d=0组合的出现次数std::unordered_map<int, int> map;for(int a : nums1){for(int b : nums2){map[a+b]++;}}int count = 0;for(int c : nums3){for(int d : nums4){auto it = map.find(0-(c+d));if( it != map.end()){count += it->second; }}}return count;}
};
  • 时间复杂度: O(n2)
  • 空间复杂度: O(n2),最坏情况下A和B的值各不相同,相加产生的数字个数为 n2

383.赎金信

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

题目链接:383. 赎金信 - 力扣(LeetCode)

思路

本题中两个字符串都是用小写字母组成,key的大小是有限制的,所以我们可以使用数组实现哈希表。数组的key是 字符串magazine中的各个字符-‘a’,value是 字符串magazine中的各个字符出现的次数

由于map需要维护红黑树或者哈希表,而且还要做哈希函数,所以很费时,而且map的空间消耗要比数组的大,所以能用数组就不map,使用数组更加简单有效

代码实现

class Solution {
public:bool canConstruct(string ransomNote, string magazine) {vector<int> record(26, 0);if(ransomNote.size() > magazine.size()){return false;}// 首先统计magazine中各个字出现的次数for(char c : magazine){record[c-'a']++;}// 然后检查这些字符是否能组成ransomNotefor(char c : ransomNote){if(--record[c-'a'] < 0) // 注意是--record,不是record--,要先减再判断{return false;}}return true;}
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

15.三数之和

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

题目链接:15. 三数之和 - 力扣(LeetCode)

这道题的测试用例有点癫😧

思路

本题的难点在于找到不重复的三元组,这就需要去重,本题与454.四数之和II是不同的

哈希法查找元素是没有顺序的,去重的时候会很麻烦,本题使用排序+双指针法

排序+双指针法

  • 初始化首先将数组排序,然后有一层for循环遍历数组,下标i从0的开始,同时定义一个下标left定义在i+1的位置上,定义下标right在数组末尾的位置上

  • 寻找过程:要在数组中找到三元组{a, b, c}使得a+b+c == 0,这里a = nums[i],b=nums[left],c=nums[right]

    接下来如何移动left 和 right 呢?有三种情况:

    • 如果a + b + c > 0,则说明nums[right]太大了,right–
    • 如果a + b + c < 0,则说明nums[left]太小了,left++
    • 如果a + b + c == 0,则找到了一个三元组{a, b, c},我们将它用一个二维数组存放起来,然后左右指针同时紧缩,即依次做right-- 和 left++
  • 结束本次查找,遍历下一个元素:当left与right相遇时,我们结束left和right的移动,然后i++,移动到下一个元素,继续进行上述步骤,直至数组遍历完了

注意:while循环终止条件是right > left还是right >= left呢?对于这样的边界条件,我们可以设身处地的想一想,当right == left时,nums[left]nums[right]是同一个元素,此时找到的a+b+c == 0的三元组没有意义,因此循环条件是right > left

执行过程如图所示:

15.三数之和

剪枝+去重

  1. 剪枝:排除后续元素全为正的数组:每次遍历都需要先判断nums[i]是否大于0。如果nums[i] > 0,则说明数组的第i位后的所有元素都是正的,不可能出现a+b+c == 0的情况

  2. 去重:对a去重,判断nums[i] == nums[i-1],若成立,则continue

    比如有数组nums = {-1, -1, 2, 2}:当i=1时,nums[i] = -1,而nums[i-1] = -1,则说明-1在之前已经使用过了,即本次查找的a 和 上一次查找的a重复了。由于 上一次查找的left和right范围{-1, 2, 2} 包含了 本次查找的left和right范围{2, 2},所以在a相同的情况下,本次查找的三元组 是 上一次查找查找的三元组的子集,这就出现了三元组的重复,所以当nums[i] == nums[i-1]成立时,直接continue,nums[i]指向下一个元素

  3. 去重:对b和c去重,每次找到一个三元组后判断。假设有数组nums = {-2, -1, -1, 3, 3},当a=-2时,按照双指针法过程,会得到两个三元组结果:{-2, -1, 3}和{-2, -1, 3}。

    为什么会出现重复呢?分析来说:第一次循环b、c的下标left=1,right=4,a+b+c=0,然后left和right同时收缩。第二次循环b、c的下标left=2,right=3,a+b+c=0,所以这个三元组也被存放到结果集中了。明显这两次的结果出现了重复,原因就在于第一次循环中,nums[right] == nums[right+1]nums[left] == nums[left-1],因此left和right同时收缩后,b和c的结果还是一样的。对b和c去重,就要判断nums[right] == nums[right+1] 或 nums[left] == nums[left-1]是否成立,如果成立,则需要对right-- 或 对left++

代码实现

class Solution {
public:vector<vector<int>> threeSum(vector<int>& nums) {vector<vector<int>> result;sort(nums.begin(), nums.end());int left, right;for(int i=0; i<nums.size(); ++i){if(nums[i] > 0){break;}// 去重aif(i > 0 && nums[i] == nums[i-1]){continue;}left = i+1;right = nums.size()-1;while(right > 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]});left++;right--;// 去重b和cwhile(right > left && nums[right] == nums[right+1]){right--;}while(right > left && nums[left] == nums[left-1]){left++;}}}}return result;}
};
  • 时间复杂度: O(n^2),可以看到使用双指针减少了一层for循环,时间复杂度降低了一个量级
  • 空间复杂度: O(1)

18.四数之和

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

题目链接:18. 四数之和 - 力扣(LeetCode)

思路

这道题和 三数之和 思路相同,都是使用排序+双指针法。注意这道题需要a+b+c+d == target,所以在剪枝上与 三数之和 有所不同。

整体思路:首先对nums进行排序,然后用两层for循环遍历数组nums,获得a = nums[i]和b = nums[j],在第二层for循环中,使用双指针left和right查找与a、b相匹配的c、d(四个变量 加和为target),去重操作与 三数之和 相同

剪枝+去重

  1. 剪枝:target可能为负值,所以用a > target剪枝是错误的,还需要加上条件a >= 0,因此剪枝条件为:a > target && a >= 0。第2级for循环的剪枝条件为:a+b > target && a+b >= 0
  2. 去重:与 三数之和 相同,对a、b去重需要判断nums[i] == nums[i-1],避免对重复的a、b进行查找;对c、d去重需要判断 nums[left] == nums[left-1] 以及 nums[right] == nums[right+1],若成立,则left++ 或 right–

注意:本题有溢出情况,需要将sum转化为long long型再进行判断

代码实现

class Solution {
public:vector<vector<int>> fourSum(vector<int>& nums, int target) {vector<vector<int>> result;sort(nums.begin(), nums.end());int left, right;for(int i=0; i<nums.size(); ++i){// 剪枝处理if(nums[i] > target && nums[i] >= 0){break;}// 对nums[i]去重if(i>0 && nums[i] == nums[i-1]){continue;}for(int j=i+1; j<nums.size(); ++j){// 2级剪枝处理if(nums[i] + nums[j] > target && nums[i] + nums[j] >= 0){break;}// 对nums[j]去重if(j > i+1 && nums[j] == nums[j-1]){continue;}left = j+1;right = nums.size()-1;while(right > left){// 测试用例中有溢出情况,所以要用long long计算sumlong long sum = static_cast<long long>(nums[i]) + nums[j] + nums[left] + nums[right];if(sum > target){right--;}else if(sum < target){left++;}else{result.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});// 找到答案时,双指针同时收缩left++;right--;// 双指针去重while(right > left && nums[right] == nums[right+1]){right--;}while(right > left && nums[left] == nums[left-1]){left++;}}}}}return result;}
};
  • 时间复杂度: O(n^3)
  • 空间复杂度: O(1)

复习:C++中的类型转换方法

static_cast

static_cast 是 C++ 中的一种类型转换运算符,用于在编译时执行显式类型转换。它提供了一种安全且明确的方式来进行类型转换,确保在转换过程中进行适当的检查,除了基本数据类型之间的转换(如int转换为double)、指针类型之间的转换(int* 转换为 double*)外,static_cast还可以进行类层次结构中的转换:

假设有一个基类 Base 和一个派生类 Derived

class Base {
public:virtual void show() {std::cout << "Base class" << std::endl;}
};class Derived : public Base {
public:void show() override {std::cout << "Derived class" << std::endl;}
};Base* basePtr = new Derived();
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 将基类指针转换为派生类指针
derivedPtr->show(); // 输出 "Derived class"

dynamic_cast

用于运行时类型检查和转换,通常用于多态类型之间的转换。适用于具有继承关系的指针或引用类型之间的转换。

class Base {virtual void foo() {} // 必须有虚函数才能使用 dynamic_cast
};
class Derived : public Base {};Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 类层次结构中的转换
if (derivedPtr) {// 成功转换
} else {// 转换失败
}

reinterpret_cast

用于在几乎所有类型之间进行位级别的转换,适用于指针类型之间、整数类型与指针类型之间的转换。使用时需谨慎,因为它可能导致未定义行为。

int a = 10;
void* ptr = reinterpret_cast<void*>(&a); // 将 int* 转换为 void*
int* intPtr = reinterpret_cast<int*>(ptr); // 将 vo

感谢 chatgpt

总结

今天完成了四道题,这四道题都比较难,需要好好体会。四数相加II 使用的方法与 day6文章中的最后一题 两数之和 思想类似,但是又有所不同,主要体现在对map的键值对的设定。赎金信比较简单,需要注意magazine的每个字符只能用一次。三数之和四数之和是今天的重点,这两道题的难点在于如何去重四数相加II不需要去重,所以比这两道题简单。使用哈希法查找元素的时候,查找的结果是无顺序的,因此去重很麻烦,这两道题使用哈希法就不合适了。这两道题更好的解法是排序+双指针双指针降低了时间复杂度的数量级。用双指针替代两层for循环,这是一个很巧妙的思想。同时要多体会两道题中的剪枝+去重操作

双指针小结

截止目前,文章中涉及到的双指针法有:

  • 27.移除元素(opens new window)
  • 15.三数之和(opens new window)
  • 18.四数之和(opens new window)

链表相关双指针题目:

  • 206.反转链表(opens new window)
  • 19.删除链表的倒数第N个节点(opens new window)
  • 面试题 02.07. 链表相交(opens new window)
  • 142题.环形链表II(opens new window)

双指针法在字符串题目中还有很多应用,后面还会介绍到

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

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

相关文章

HarmonyOS(45) 控件拖动或者拖拽PanGesture

PanGesture实现控件拖动的效果&#xff0c;通过拖动的坐标位置调用position或者translate方法来更新UI的位置。效果见下图&#xff1a; 具体代码如下&#xff1a; // xxx.ets Entry Component struct PanGestureExample {State offsetX: number 0State offsetY: number 0pos…

【AI学习指南】轻量级模型-用 Ollama 轻松玩转本地大模型

目录 探索 最小的AI模型 发现 其他轻量级模型 用 Ollama 轻松玩转本地大模型 本地大模型 Ollama 快速上手 安装 手动安装 下载ollama二进制文件 添加 Ollama 作为启动服务(推荐) 安装 CUDA 驱动程序(可选 - 适用于 Nvidia GPU) 安装 ROCm(可选 - 对于 Radeo…

Typesript的type和interface的异同?

详解TypeScript中type与interface的区别_javascript技巧_脚本之家 一、相同的地方 1、都可以用来定义对象&#xff0c;描述函数 我们在用typescript开发的时候经常要用到数据类型定义&#xff0c;比如我们写一个接口或者函数的时候定义传参数据类型及字段等。这样子方便知道这…

代码随想录day21 二叉树最后一天 || 669修剪二叉树 108将有序数组转变为平衡搜索二叉树 538把搜索二叉树变为累加二叉树

669修剪二叉树 力扣题目链接 题目描述&#xff1a; 给你二叉搜索树的根节点 root &#xff0c;同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树&#xff0c;使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即&#xff0c;如果…

K8s 核心组件——API Server

1. Kubernetes API Server 概述 1.1 基本概念 Kubernetes API Server&#xff08;API Server&#xff09;是 Kubernetes 的核心组件之一&#xff0c;负责暴露 Kubernetes API 给用户和客户端&#xff0c;接收和处理来自客户端的请求&#xff0c;并将其存储到 etcd 中。Kubern…

花几千上万学习Java,真没必要!(三十一)

集合&#xff1a; 1、数组的特性&#xff1a; 2、ArrayList&#xff08;动态数组&#xff09;&#xff1a; 测试代码1&#xff1a; package listtest.com; import java.util.ArrayList; import java.util.List; public class HashCodesArrayList { public static void ma…

深入分析 Android ContentProvider (六)

文章目录 深入分析 Android ContentProvider (六)ContentProvider 的性能优化和实践案例&#xff08;续&#xff09;1. 性能优化技巧&#xff08;续&#xff09;1.6. 使用批量插入优化性能示例&#xff1a;批量插入实现 1.7. 使用 Projections 优化查询示例&#xff1a;使用 Pr…

本周热点丨快手《可灵》国际版正式上线、《王者荣耀》国际服海外下载量破5000万

7月第4周 秦彻带动《恋与深空》2.0霸榜全球 《蔚蓝档案》上线3.5周年庆典版本&#xff0c;登顶日服iOS畅销榜 《Honor of Kings》下载量突破五千万 英伟达将开发"Blackwell"的中国定制版芯片 人工智能初创公司Cohere获得5亿美元融资 Fal AI推出全新开源图像生成模型A…

reshape函数及MATLAB应用

reshape 函数在 MATLAB 中是一个非常有用的函数&#xff0c;通过重新排列现有元素来重构数组。它允许你重新调整数组&#xff08;或矩阵&#xff09;的尺寸&#xff0c;而不改变其数据。这个函数特别适用于当你需要将一个矩阵或数组从一种结构转换为另一种结构时&#xff0c;只…

AFSim 仿真系统--任务处理器入门指南

任务处理器 任务处理器&#xff08;WSF_TASK_PROCESSOR&#xff09;是一个WSF处理器&#xff0c;它提供了检查轨迹管理器中的数据&#xff08;轨迹&#xff09;并对其采取行动的能力。这些行动包括&#xff1a; *向下属分配任务 *激活或停用传感器或干扰器 *开火武器 *操纵平台…

昇思MindSpore 应用学习-DCGAN生成漫画头像-CSDN

日期 心得 昇思MindSpore 应用学习-DCGAN生成漫画头像&#xff08;AI代码学习&#xff09; DCGAN生成漫画头像 在下面的教程中&#xff0c;我们将通过示例代码说明DCGAN网络如何设置网络、优化器、如何计算损失函数以及如何初始化模型权重。在本教程中&#xff0c;使用的动…

vue2和el-input无法修改和写入,并且不报错

文章目录 一. 业务场景描述二. 原因分析三.解决方案3.1 方案一 原生标签&#xff08;不建议&#xff09;3.2 方案二 父子传递&#xff08;不建议&#xff09;3.3 方案三 vuex&#xff0c;pinia 状态传值&#xff08;不建议&#xff09;3.4 方案四 vue初始化属性 &#xff08;建…

Java从入门到精通(十三)~ 枚举和注解

晚上好&#xff0c;愿这深深的夜色给你带来安宁&#xff0c;让温馨的夜晚抚平你一天的疲惫&#xff0c;美好的梦想在这个寂静的夜晚悄悄成长。 文章目录 目录 前言 一、枚举 1.1 枚举的概念 ​编辑 1.2 枚举的特点 1.3 枚举的实际运用 1. 状态机&#xff0c;描述属性的…

Nginx配置详解

1. Nginx安装 进入到/usr/local目录下&#xff0c;执行命令&#xff1a; yum install -y nginx通过如下命令启动nginx服务 systemctl start nginx通过如下命令&#xff0c;设置nginx开启自启动 systemctl enable nginxnginx默认占用80端口。 访问ip:80&#xff0c;可以看到…

Open3D 点云按xyz轴等距切片

目录 一、概述 1.1原理 1.2实现步骤 1.3应用 二、代码实现 2.1关键函数 2.2完整代码 三、实现效果 3.1原始点云 3.2按x轴切片 3.3按y轴切片 3.4按z轴切片 Open3D点云算法汇总及实战案例汇总的目录地址&#xff1a; Open3D点云算法与点云深度学习案例汇总&#xff…

Java中线程池的基本使用

参考这个课程&#xff0c;讲的非常详细 27.线程池使用步骤总结_哔哩哔哩_bilibili 应用场景 商品秒杀 文件上传 订票系统 自定义线程池&#xff08;让我们了解底层逻辑的&#xff09; 构造方法 饱和机制 就是我们的队列饱和后我们该怎么处理 是拒绝这个任务还是啥…

Aria2 任意文件写入漏洞

目录 Aria2介绍漏洞描述漏洞复现 Aria2介绍 Aria2是一个在命令行下运行&#xff0c;多协议&#xff0c;多来源下载工具&#xff08;HTTP / HTTPS&#xff0c;FTP&#xff0c;BitTorrent&#xff0c;Metalink&#xff09;&#xff0c;内建XML-RPC用户界面。Aria提供RPC服务器&a…

流媒体服务器一:使用成熟的流媒体SRS 搭建 RTMP流媒体服务器

1 安装和测试srs流媒体服务器 服务器&#xff1a;SRS(Simple RTMP Server&#xff0c;⽀持RTMP、HTTP-FLV&#xff0c;HLS) 推流端&#xff1a;ffmpeg OBS 拉流端&#xff1a;ffplay VLC srs播放器 1.1 安装srs流媒体服务器 官网 SRS (Simple Realtime Server) | SRS 码…

18746 逆序数

这个问题可以使用归并排序的思想来解决。在归并排序的过程中&#xff0c;我们可以统计逆序数的数量。当我们合并两个已经排序的数组时&#xff0c;如果左边的数组中的元素&#xfffd;&#xfffd;于右边的数组中的元素&#xff0c;那么就存在逆序&#xff0c;逆序数的数量就是…

IEC104转MQTT网关支持MQTT对接Ignition、阿里云、华为云、亚马逊AWS、ThingsBoard、Zabbix平台助力远程管理

钡铼推出了一款专为电力系统设计的数据采集网关——IEC104转MQTT网关BE113&#xff0c;感兴趣的朋友接着往下看&#xff1a; 一、网关概述 IEC104转MQTT(Ignition)网关BE113是一款专为电力系统设计的数据采集网关&#xff0c;支持DL/T645和IEC104等协议的设备。上行支持MQTT协…