leetcode:1,454,15,18
上一章遗留的两数之和和这一章的三数之和四数之和四数相加一起看。
首先从代码随想录的一道思考题开始
思考题
既然三数之和可以使用双指针法,我们之前讲过的1.两数之和 (opens new window),可不可以使用双指针法呢?
如果不能,题意如何更改就可以使用双指针法呢? 大家留言说出自己的想法吧!
两数之和 就不能使用双指针法,因为1.两数之和 (opens new window)要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。
如果1.两数之和 (opens new window)要求返回的是数值的话,就可以使用双指针法了。
首先,什么时候适合用双指针来解决问题?
双指针(two pointers)是算法中常用的一个技巧,尤其在数组或链表这样的线性结构的问题中。以下是几种适合使用双指针解决的场景:
-
数组或链表的有序性:当数组或链表有序时,双指针可以帮助我们快速找到解或确定没有解。例如,在有序数组中查找两个数,使其之和为一个给定的值。
-
需要滑动窗口的问题:当需要找到满足某种条件的子数组或子串时,可以使用双指针来维护一个滑动窗口。例如,求解数组或字符串中的最小覆盖子串。
-
需要删除或修改某些元素:双指针可以用来同时读取和修改数组或链表,例如移除数组中的某个元素或是链表的逆转。
-
链表问题中找到中间节点或检测环:使用快慢指针,其中一个指针移动的速度是另一个的两倍,可以帮助我们解决这些问题。
-
需要合并两个有序列表或数组:例如,在归并排序中,我们可以使用双指针从两个已排序的子列表中取得最小值。
-
需要找到某种特定结构或模式:例如,在数组中找到三个数,使其之和为0。可以使用双指针来缩小搜索范围。
-
数组去重:当需要对数组进行去重操作时,双指针技巧可以有效地实现在原地修改,无需额外的空间。
以上是双指针的常见应用场景。当然,其真正的应用范围远不止这些,但这些场景可以为你提供一个方向,使你能更好地理解何时应该考虑使用双指针来解决问题。
为什么两数之和这道题不能用双指针而三数之和这道题就可以
对于三数之和,一个有效的方法是先对数组排序,然后固定一个元素,对剩余的部分使用双指针来寻找和为 -nums[i]
的两个数。排序可以确保找到的三元组不重复,并且双指针方法在每次固定元素后,可以在 O(n) 的时间复杂度内找到所有满足条件的两个数。
简而言之,两者之间的主要区别在于,"三数之和"问题中排序是解决问题的一个必要步骤,而在"两数之和"问题中,如果数组初始是无序的,排序则是一个额外的开销。
所以使用双指针的场景可不可以大概分为两类:一是数组是有序的。二是无序但是需要对数组进行修改
ChatGPT
是的,您的总结很到位。双指针技巧在很多算法问题中都非常有用,其主要应用场景确实可以大致归纳为以下两类:
-
基于数组或链表的有序性:
- 这通常用于查找、匹配或其他需要利用有序性的场合。在有序的数组或链表中,我们可以根据两指针指向的值或它们的相对位置,来决定如何移动指针。
- 例如:“两数之和”在有序数组中的情况。
- 或者,“对撞指针”在处理如“回文字符串”检查等问题时。
- 这通常用于查找、匹配或其他需要利用有序性的场合。在有序的数组或链表中,我们可以根据两指针指向的值或它们的相对位置,来决定如何移动指针。
-
需要对数组或链表进行修改:
- 这类问题中,双指针常常用作“读写指针”。其中一个指针用于遍历(读),另一个指针用于确定修改或写入的位置(写)。
- 例如:“移除元素”、“移动零”或“删除排序数组中的重复项”这类问题。
- 这类问题中,双指针常常用作“读写指针”。其中一个指针用于遍历(读),另一个指针用于确定修改或写入的位置(写)。
当然,双指针的应用场景远不止这些,但这两类是最常见的。理解这两类场景可以帮助我们更好地判断和应用双指针技巧来解决实际问题。
为什么哈希表能降低时间复杂度
我有一些明白了,相当于哈希表提供了一个类似记录数据的功能,即哈希表有数组所没有的contains这个方法。依靠这个减少了循环次数对吗
ChatGPT
是的,您的理解是正确的!
哈希表的主要优势是它能够在平均情况下以近乎常数时间 �(1)O(1) 进行查找、插入和删除操作。这与数组或列表的查找操作(平均时间复杂度为 �(�)O(n))形成了鲜明的对比。这是因为哈希表内部使用了一个称为“哈希函数”的函数,它可以直接将输入(通常是键)映射到存储桶的位置。
具体到“两数之和”这个问题,如果我们不使用哈希表,我们通常需要对每个元素执行一个 �(�)O(n) 的查找操作以确定其补数是否存在,这导致总的时间复杂度为 �(�2)O(n2)。但使用哈希表,我们可以在 �(1)O(1) 时间内确定补数是否存在,这使得总的时间复杂度下降到 �(�)O(n)。
所以,正如您所说的,哈希表为我们提供了一个高效的“包含”(或“查找”)功能,使我们能够显著减少必要的循环次数,从而提高算法的效率。
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
首先是自己的思考:为什么哈希表能降低时间复杂度?(理解这个对理解代码随想录写的东西很有帮助)
我有一些明白了,相当于哈希表提供了一个类似记录数据的功能,即哈希表有数组所没有的contains这个方法。依靠这个减少了循环次数对吗?
ChatGPT
是的,您的理解是正确的!
哈希表的主要优势是它能够在平均情况下以近乎常数时间 �(1)O(1) 进行查找、插入和删除操作。这与数组或列表的查找操作(平均时间复杂度为 �(�)O(n))形成了鲜明的对比。这是因为哈希表内部使用了一个称为“哈希函数”的函数,它可以直接将输入(通常是键)映射到存储桶的位置。
即哈希表提供了索引的功能
具体到“两数之和”这个问题,如果我们不使用哈希表,我们通常需要对每个元素执行一个 �(�)O(n) 的查找操作以确定其补数是否存在,这导致总的时间复杂度为 �(�2)O(n2)。但使用哈希表,我们可以在 �(1)O(1) 时间内确定补数是否存在,这使得总的时间复杂度下降到 �(�)O(n)。
所以,正如您所说的,哈希表为我们提供了一个高效的“包含”(或“查找”)功能,使我们能够显著减少必要的循环次数,从而提高算法的效率。
在这里出现了疑惑还是因为哈希表的基础理论没有打好,可以看代码随想录的哈希表理论:
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
即在数组中不存在的contains方法
代码随想录思路:
首先我再强调一下 什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
本题呢,我就需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。
那么我们就应该想到使用哈希法了。
因为本题,我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
再来看一下使用数组和set来做哈希法的局限。
- 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
- set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value再保存数值所在的下标。
本题其实有四个重点:
- 为什么会想到用哈希表
- 哈希表为什么用map
- 本题map是用来存什么的
- map中的key和value用来存什么的
leetcode官方思路:
方法二:哈希表
思路及算法
注意到方法一的时间复杂度较高的原因是寻找 target - x 的时间复杂度过高。因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。
使用哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N)O(N)O(N) 降低到 O(1)O(1)O(1)。
这样我们创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。
先看一个错误代码,这个代码错误在顺序错了,例如,考虑这个情况:nums = [3, 2, 4]
,target = 6
。对于i = 0
(即nums[i] = 3
),您将3放入哈希表,并立即检查是否存在6 - 3 = 3
,找到了,但这并不是正确的答案。
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], i);
int k = target-nums[i];
if(map.containsKey(k)){
return new int[]{i,map.get(k)};
}
}
return new int[0];
}
}
正确的代码
lass Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int k = target-nums[i];
if(map.containsKey(k)){
return new int[]{i,map.get(k)};
}
map.put(nums[i], i);
}
return new int[0];
}
}
为了更严谨可以加上
int[] res = new int[2];
if(nums == null || nums.length == 0){
return res;
自己对于数组和哈希表的思考
为什么数组里找一个值就需要从头开始遍历,是不是因为数组只给了数组第一个数的某个东西
ChatGPT
您的观察很有趣。让我们深入探讨一下数组的工作原理,以及为什么查找一个未知位置的值需要从头开始遍历。
-
数组的内部工作:
- 在底层,数组其实是一个连续的内存块。每个元素在内存中的位置是通过基地址(即数组的起始地址)加上偏移量来确定的。这个偏移量基本上是索引乘以每个元素的大小。
- 例如,如果你有一个整数数组(每个整数通常占用4字节)并且你想访问第3个元素,计算机会查看数组的基地址,然后跳过前8个字节(2个整数的大小)来找到第3个整数。
-
数组的索引:
- 数组的一个主要特点是快速随机访问:如果你知道元素的索引,你可以在�(1)O(1)的时间内直接访问它。
- 但是,如果你只知道元素的值而不知道它的索引,那么你没有办法直接计算出它在内存中的位置。因此,你必须从头开始检查每个元素,直到找到匹配的值。
-
为什么不能直接跳到值的位置?
- 这是因为数组没有为每个可能的值提供一个直接的映射到其索引的机制。换句话说,数组并没有存储值到索引的映射。数组只保留了索引到值的映射。
在与哈希表的比较中,哈希表恰好提供了值(或键)到索引的映射。这就是为什么哈希表可以迅速地找到一个值(或键),而数组在未知索引的情况下则不能。
总之,您是对的,数组的设计使其只能快速地通过索引访问值。如果要查找特定的值,必须从头开始遍历,除非使用了其他数据结构或方法来帮助查找。
为什么数组里找一个值就需要从头开始遍历,而哈希表不用遍历
ChatGPT
要理解这一点,我们需要了解数组和哈希表的工作原理和它们如何存储及访问数据。
-
数组:
- 存储方式: 数组是一个连续的内存块,其中每个元素按照其索引进行存储。
- 查找值: 如果我们不知道值的索引,我们就没有办法直接跳到这个值的位置。因此,必须从头开始,检查每个元素,直到找到我们要找的值。这是一个线性操作,时间复杂度为�(�)O(n)。
-
哈希表:
- 存储方式: 哈希表使用一个哈希函数将“键”转化为数组(通常称为“桶”或“表”)的索引。这个索引决定了“值”在数组中的存储位置。
- 查找值: 当我们想要查找一个值时,我们首先使用哈希函数计算其对应的“键”的哈希值,这会给我们一个索引。然后,我们可以直接跳到数组的那个位置来检查和取出值。理论上,这是一个常数时间操作,时间复杂度为�(1)O(1)。但是,需要注意的是,当存在哈希冲突(两个或多个键哈希到同一个索引)时,可能需要额外的时间来解决冲突,例如通过链表或其他方法。
所以,关键的区别在于哈希表使用哈希函数提供了从“键”到其在内部数组中的位置的直接映射。这意味着,不需要线性地搜索整个结构来查找一个值。相反,数组没有这样的映射功能,所以当我们需要查找一个值时,必须遍历它。
总之,数组的查找基于线性搜索,而哈希表的查找基于哈希函数提供的直接映射,这使得在平均情况下,哈希表的查找效率要高得多。
也就是说关键点在与存储方式有区别,数组就是单纯的存储进去并知道起始位置和偏移量。而利用哈希表存入的时候,相当于又建了一个表,根据你要存入数据的哈希值等来得到一个存在这个表里的索引。所以判断是不是有自己要找的值,直接看这个索引对应处有没有值就可以了。也就是说创建了一个表把我们要找的值变成了索引对吗
您的理解基本上是正确的,让我为您澄清一些细节:
-
数组:
- 如您所说,数组是一个连续的内存块,我们通过索引直接访问每个位置。如果我们知道索引,那么访问是非常快的,几乎是即时的。但是,如果我们想要查找一个特定的值,并不知道其索引,那么就必须从头开始检查每个位置。
-
哈希表:
- 当您说哈希表存储数据时“建了一个表”,您实际上描述的是哈希表的内部结构,通常称为“桶”或“数组”。确实,哈希表内部有一个数组。
- 当我们想要在哈希表中存储一个键-值对时,我们首先使用哈希函数计算键的哈希值。这个哈希值然后被转换(例如,通过模运算)为数组的一个索引。然后,键-值对被存储在这个索引对应的位置。
- 当我们想要查找一个键对应的值时,我们再次使用哈希函数来计算这个键的哈希值,并确定它应该在数组中的哪个位置。然后,我们直接访问那个位置来获取值。
- 因此,实际上,哈希表将“键”转换为一个索引,从而允许我们在平均情况下在常数时间内访问任何给定的键-值对。
但是,需要注意的是,在实际应用中,可能会存在两个不同的键具有相同的哈希值,这称为哈希冲突。为了处理这种情况,哈希表内部需要有某种机制,如链表或开放地址法等。因此,在实际查找时,可能会有一些额外的操作。
所以,总结一下,您是对的:哈希表的关键之处在于它使用哈希函数将键转换为数组的索引,从而实现快速查找。而数组没有这样的映射功能,所以查找一个值需要遍历整个数组。
为什么这道题不用哈希,根据代码随想录视频是因为去重复杂
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums);
for (int first = 0; first < nums.length; first++) {
if (first > 0 && nums[first] == nums[first - 1]) {
continue;
}
int second = first + 1;
int third = nums.length - 1;
while (second < third) {
if (nums[first] + nums[second] + nums[third] > 0) {
third--;
} else if (nums[first] + nums[second] + nums[third] < 0) {
second++;
} else {
List<Integer> list = new ArrayList<>();
list.add(nums[first]);
list.add(nums[second]);
list.add(nums[third]);
ans.add(list);
while (third > second &&nums[third] == nums[third - 1]) {
third--;
}
while (third > second &&nums[second] == nums[second + 1]) {
second++;
}
second++;
third--;
}
}
}
return ans;
}
}
这个代码里有几个细节:
1.对first进行去重的时候,为什么是i和i-1比较而不是和i+1,例子是{-1,-1,2}如果是后者则相当于first和second在比较,得到的结果是和为0且结果里没有重复数字。与左边比较才是排除了重复的first。
2.while循环里second和third中间符号有没有等于好。答案是没有,显然相等的时候不符合题意,在这种情况下不能够进入循环。
3.second和third去重要放在收集结果的后面。
4.while (third > second &&nums[third] == nums[third - 1]) { third--; }这个判断条件里不加third > second会怎样 考虑一个例子{0,0,0,0,0}.
5.如果将first去重里的
具体来说,对于不同类型的循环,continue
的行为是:
- 对于
for
循环:当遇到continue
时,循环的增量部分(例如i++
)会立即执行,然后开始下一个迭代。 - 对于
while
和do-while
循环:当遇到continue
时,控制权直接回到循环的条件判断,继续执行下一个迭代。
什么时候用哈希什么时候用排序加双指针
选择使用哈希表还是排序加双指针的策略,通常取决于具体的问题需求和所需的操作。以下是一些建议和情境,帮助你决策:
使用哈希表:
-
需要原始索引:如果问题需要返回原始索引,如“两数之和”,哈希表更为方便,因为排序会改变元素的原始位置。
-
查找操作优先:哈希表在最佳情况下为常数时间 �(1)O(1) 完成查找操作。如果问题中需要频繁的查找操作,哈希表通常更有效。
-
不关心顺序:如果解决问题不需要关心元素的顺序,哈希表可能是个好选择。
-
需要处理重复元素:哈希表可以用来跟踪元素的出现次数,这在处理重复元素或者需要计数的场景中很有用。
使用排序加双指针:
-
需要顺序:对于一些问题,排序数组可以简化解决方案。例如,“三数之和”或其他需要查找组合或子序列的问题。
-
需要去重:在排序的数组中去重更为直接,因为重复的元素会聚集在一起。
-
空间敏感:双指针通常使用恒定的额外空间,而哈希表需要的额外空间会随着元素的数量增加。如果问题对空间复杂度有严格要求,双指针可能是更好的选择。
-
有序数据输入:如果输入数据已经是有序的,或者排序本身是解决问题的一部分(例如找中位数),那么排序和双指针可能是更直观的方法。
综合:
有时,结合使用这两种策略可能是最佳选择。例如,你可能首先使用哈希表来预处理数据,然后再使用排序和双指针来完成其他操作。
最终,选择哪种策略取决于问题的具体需求、数据的性质以及你对不同方法的熟悉程度。随着经验的积累,选择最佳策略会变得更加直观。
public class Leetcode18 {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ans = new ArrayList<>();
if (nums == null || nums.length < 4) {
return ans;
}
Arrays.sort(nums);
for (int i = 0; i < nums.length-3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
if ((long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
break;
}
if ((long) nums[i] + nums[nums.length - 3] + nums[nums.length - 2] + nums[nums.length - 1] < target) {
continue;
}
for (int j = i + 1; j < nums.length-2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
if ((long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
break;
}
if ((long) nums[i] + nums[j] + nums[nums.length - 2] + nums[nums.length - 1] < target) {
continue;
}
int left = j + 1;
int right = nums.length - 1;
while (left < right) {
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
List<Integer> temp = new ArrayList<>();
temp.add(nums[i]);
temp.add(nums[j]);
temp.add(nums[left]);
temp.add(nums[right]);
ans.add(temp);
while (left < right && nums[right] == nums[right - 1]) right--;
while (left < right && nums[left] == nums[left + 1]) left++;
left++;
right--;
}
}
}
}
return ans;
}
}
相比于代码随想录的答案,这个答案的剪枝操作更棒。
以下是这道题的几个要点:(一共四点)
1.int left = j + 1; int right = nums.length - 1;这两句要写在循环里面。
2.j的范围应该是从>i+1开始,这个一定不是常数(还是基础不好,这个是这道题的逻辑问题)比如[2,2,2,2,2],如果j从0开始检查,就会忽视掉第一个答案(和代码随想录里讲的为什么i的判断是从>0开始一个道理)写成大于1这个例子[-2,-1,-1,1,1,2,2]就会出错,是因为如果我们使用 j > 1
作为条件来检查重复,那么当 i
移动到 -1
并且 j
移动到紧接着的 -1
时,这个条件会使得这个 -1
被跳过,因为它和前一个 -1
相同。这就会导致 [-1,-1,1,1]
这个解被忽略。
3.注意这句话要写成long
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
对于Java中的整数类型int
,其取值范围是 −231−231 到 231−1231−1。具体来说,范围是:
−2,147,483,648−2,147,483,648 到 2,147,483,6472,147,483,647
或大约是 −2×109−2×109 到 2×1092×109
所以,当你预计的计算可能导致结果超过这个范围时,你应该考虑使用long
。具体到你的问题,对于四数之和,如果最大可能值是 4×1094×109,那么这已经超过了int
的最大值,因此你需要使用long
。
一般来说,如果你知道你的计算可能会接近或超过 2×1092×109,那么最好使用long
。这为你提供了一个额外的保险,确保不会发生意外的整数溢出。
long和int的转换规则是怎样的
在Java中,int
和 long
之间的转换遵循以下规则:
-
从
int
到long
的隐式转换:当你把一个
int
类型的值赋给一个long
类型的变量时,会发生自动的类型提升,也就是说,int
会被隐式地转换为long
。这是安全的,因为long
的范围比int
大。javaCopy code
int intVal = 42; long longVal = intVal; // 隐式转换
-
从
long
到int
的显式转换:反过来,如果你想把一个
long
类型的值赋给一个int
类型的变量,你需要进行显式转换,因为long
的范围比int
大,这可能会导致数据丢失。使用类型转换操作符(int)
来完成这个转换。javaCopy code
long longVal = 42L; int intVal = (int) longVal; // 显式转换
但请注意,如果
long
的值超出了int
的范围,那么转换的结果可能不会如你所期望。这种情况下,数值会被截断到int
的范围,结果可能是不正确的。 -
字面量的后缀:
当使用字面量表示一个长整数时,你可以通过在数字后加上一个
L
或l
后缀来指示这是一个long
类型的字面量。尽管l
是有效的,但推荐使用大写的L
,因为小写的l
很容易和数字1
混淆。javaCopy code
long longVal = 1234567890L;
总之,从int
到long
的转换是隐式的,而从long
到int
的转换需要显式地使用类型转换操作符。但当从较大的数据类型转换为较小的数据类型时,应始终小心,避免数据丢失或不期望的结果。
那为什么这道题的代码里要在前面加上(long)
在Java中,当你进行两个整数的算术运算时(例如加法或乘法),计算的结果会默认为int
类型。如果这两个整数的计算结果超出了int
的范围,即使你将结果存储在一个long
类型的变量中,仍然会发生溢出。
通过在计算之前显式地将其中一个操作数转换为long
,你确保整个计算都在long
的范围内进行,从而避免了溢出的风险。
以这个代码为例:
javaCopy code
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
这里,即使nums[i]
, nums[j]
, nums[left]
, 和 nums[right]
都是int
类型的,但由于nums[i]
被显式地转换为long
,整个加法运算会在long
的范围内进行,保证结果不会因为超出int
的范围而溢出。
如果不加(long)
,则四个int
值之间的加法会默认在int
的范围内进行,然后再将结果赋给long
变量sum
。如果加法的结果超出了int
的范围,那么即使结果类型是long
,也会存储一个溢出的、不正确的值。这就是为什么在计算之前进行类型转换是很重要的原因。
也就是说我的nums【i】是int来定义的,如果一开始就是long定义的就没问题了
是的,你完全正确。
4.将for (int i = 0; i < nums.length; i++)
更改为for (int i = 0; i < nums.length - 3; i++)
,将for (int j = i + 1; j < nums.length; j++)
更改为for (int j = i + 1; j < nums.length - 2; j++)
。这样可以确保在检查i + 3
和j + 2
时不会超出数组的边界。
给你四个整数数组 nums1
、nums2
、nums3
和 nums4
,数组长度都是 n
,请你计算有多少个元组 (i, j, k, l)
能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0z
class Solution { public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) { int ans =0; Map<Integer,Integer> map = new HashMap<>(); for (int i: nums1){ for (int j: nums2){ int sum = i+j; map.put(sum,map.getOrDefault(sum,0)+1); } } for (int k: nums3){ for (int l:nums4){ ans=ans+map.getOrDefault(0-k-l,0); } } return ans; } }
只有一点需要注意:ans=ans+map.getOrDefault(0-k-l,0);这一句直接写成get可能会得到null而导致NullPointerException(表示你试图对一个
null
引用执行非法操作)
这道题和242对比起来看
给你两个字符串:ransomNote
和 magazine
,判断 ransomNote
能不能由 magazine
里面的字符构成。
如果可以,返回 true
;否则返回 false
。
magazine
中的每个字符只能在 ransomNote
中使用一次。
自己的代码:
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
if(ransomNote.length()>magazine.length()){
return false;
}
Map<Character, Integer> map = new HashMap<>();
for (int i = 0; i < magazine.length(); i++) {
map.put(magazine.charAt(i), map.getOrDefault(magazine.charAt(i), 0) + 1);
}
for (int i = 0; i < ransomNote.length(); i++) {
map.put(ransomNote.charAt(i),map.getOrDefault(ransomNote.charAt(i),0)-1);
}
for (int value: map.values()){
if(value<0){
return false;
}
}
return true;
}
}
就是记不住的数组的解法
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
if(ransomNote.length()>magazine.length()){
return false;
}
int[] record = new int[26];
for (int i = 0; i < magazine.length(); i++) {
record[magazine.charAt(i)-'a']++;
}
for (int i = 0; i < ransomNote.length(); i++) {
record[ransomNote.charAt(i)-'a']--;
}
for (int i:record){
if(i<0) return false;
}
return true;
}
}
计算magazine中每个字母的出现次数并将它们存储在record
数组中。