383.赎金信
题目
给你两个字符串:ransomNote
和 magazine
,判断 ransomNote
能不能由 magazine
里面的字符构成。如果可以,返回 true
;否则返回 false
。magazine
中的每个字符只能在 ransomNote
中使用一次。
代码
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
int[] hash = new int [26]; //存储小写字母的哈希值
//遍历r,字母出现一次,对应hash值-1
for(int i=0;i < ransomNote.length();i++){
hash[ransomNote.charAt(i) - 'a']--;
}
//遍历m,字母出现一次,对应hash值+1
for(int i=0;i < magazine.length();i++){
hash[magazine.charAt(i) - 'a']++;
}
//遍历hash,如果全部大于等于0,说明r属于s
for(int i=0;i < hash.length;i++){
if(hash[i] < 0){ //某个字母哈希值小于0,说明s不够
return false;
}
}
return true;
}
}
总结
1.原理
(1)为什么用hash表
题目的核心是判断r的每个小写字母是否在s中出现。其实就是r包含于s中。和字母异位词题目类似。
(2)为什么用数组hash
小写字母长度受限
(3)算法流程
初始化一个长度为26的int型hash数组,默认值为0。先变量字符串r,每一个字符,都让对应的hash值--。再遍历字符串s,每一个字符,都让对应的hash值++。最后,遍历hash数组,如果存在值小于0的,说明r比较多,s不够了,返回false。最后hash数组全大于等于0,返回true。
2.语法问题
(1)String长度:str.length()
(2)获取String第i个字符:str.charAt(i)
第二次刷题总结
1.算法没问题,就是对String进行遍历的时候,语法山上不能用char c: str的增强for,不然编译会出错,还是乖乖的写普通的for循环。
15.三数之和
题目
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
代码(算法去重)
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for(int i=0; i < nums.length - 2; i++){
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
int j = i + 1;
int k = nums.length - 1;
while(j < k){
int sum = nums[i]+nums[j]+nums[k];
if(sum == 0){
//res.add(Stream.of(nums[i],nums[j],nums[k]).collect(Collectors.toList()));
res.add(Arrays.asList(nums[i],nums[j],nums[k]));
while(j < k && nums[j] == nums[j+1]){
j++;
}
while(j < k && nums[k] == nums[k-1]){
k--;
}
j++;
k--;
}
else if(sum > 0){
k--;
}
else{
j++;
}
}
}
return res;
}
}
代码(用set去重通不过版)
这个代码是很拧巴写出来的,没有排序数组,直接硬生生的用set去重了。但是时间复杂度也很高,力扣上会超时。只是刚刚学了stream流,想操作一下列表,set的转换。
注意:每一个元组输出是其实是递增有序的,如果按这个方法不对数组排序直接找,找到之后,也要把对应的这个list递增排序一下,再add到set中,不然set里面的list都是乱的。如下图。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Set<List<Integer>> set = new HashSet<>();
for(int i=0; i < nums.length; i++){
int j;
int k;
for(j=i+1; j < nums.length; j++){
for(k=j+1; k < nums.length; k++){
int sum = nums[i] + nums[j] + nums[k];
if(sum == 0){
List<Integer> list = Stream.of(nums[i],nums[j],nums[k]).collect(Collectors.toList());
Collections.sort(list);
set.add(list);
}
}
}
}
List<List<Integer>> res = set.stream().collect(Collectors.toList());
return res;
}
}
代码(用set去重通过版)
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Set<List<Integer>> set = new HashSet<>();
Arrays.sort(nums);
for(int i=0; i < nums.length; i++){
int j = i + 1;
int k = nums.length - 1;
while(j < k){
int sum = nums[i] + nums[j] + nums[k];
if(sum == 0){
List<Integer> list = Stream.of(nums[i],nums[j],nums[k]).collect(Collectors.toList());
set.add(list);
j++;
k--;
}
else if(sum > 0){
k--;
}
else{
j++;
}
}
}
List<List<Integer>> res = set.stream().collect(Collectors.toList());
return res;
}
}
总结
1.原理
(1)为什么不能用hash法
题目要求是找一个int数组中满足a+b+c=3的所有情况,且每个情况是唯一的。如果用hash法,类似于两数之和==target,可以用一个map哈希表,key存储ab之和,value存储对应a和b的值。然后,我们用两个for循环遍历a和b,在map哈希中寻找-(a+b)是否存在。
但这里存在两个问题,第一,map的key是唯一了,可能存在a1+b1=0且a2+b2=0,但是map中key=0的情况只能存储一次,不能把所有ab的情况存下来。第二,结果要求三元组不重复,这里也不能保证。
(2)为什么用双指针法
因为这里最后返回的只是abc的数值,不用返回其对应的下标,所以可以对数组进行排序,然后用双指针。
(3)双指针法算法流程
首先,对nums进行升序排序。然后,用for循环遍历nums,索引i代表a,相当于a先固定,再去[i+1,length-1]区间里边找所有满足b+c==-a 的情况。
那怎么找呢。我们将left指向i+1,right指向length-1。当left<right时,循环计算sum=a+b+c。如果sum大于0,说明c太大了,就让right--。如果sum小于0,说明b太小了,left++。如果sum等于0,就把当前的abc加入list列表。
(4)如何去重
首先如何对a去重,就是刚进入i的for循环之后,判断nums[i]和nums[i-1]的值是否一样,如果一样,用continue跳出当前for循环。因为当索引=i,a固定时,我们会在[i+1,length-1]区间里边找所有满足b+c==-a 的情况。而在上一轮索引=i-1时,我们会在[i,length-1]区间里边找所有满足b+c==-a 的情况,这里的情况肯定能把下一轮包括进去,所以可以直接continue。注意,要求i不能是0。
其次,如何对b和c去重,就是当left和right正好指向符合要求的b和c,即a+b+c==0时,我们会把当前的情况add进结果。然后我们要循环判断如果nums[left]==nums[left+1],要让left++,如果nums[right]==nums[right-1],要让right++,也就是把b和c出现相同值的情况给跳过。
2.注意点
(1)首先要对b和c进行去重时,循环里面的left < right条件不能省略,不然right和left一直移动,可能会导致下标越界。
(2)对b和c的while循环去重结束, right--和left++不能漏掉, 因为去重只是让b和c指向最后一个相同的元素,而不是新的元素,因此right和left还要指向新的元素,然后继续不断靠近。
3.优化点
(1)进入for循环后,可以提前剪枝判断,如果nums[i] > 0,可以直接return result,因为如果a大于0,因为递增排序了,a+b+c必然大于0
(2)for循环的区间可以缩小到nums.length - 2,是因为a最多指向倒数第三个元素,后边两个元素要留给b和c。
4.语法
(1)如何对int数组进行排序:Arrays.sort(nums)
(2)如何把a,b,c转为list:Arrays.asList(nums[i], nums[left], nums[right])
第二次刷题总结
1.这次写的代码更简洁一点了,附在下面。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
for(int i=0; i < nums.length; i++){
if(i > 0 && nums[i] == nums[i - 1]){
continue;
}
int j = i + 1;
int k = nums.length - 1;
while(j < k){
int sum = nums[i] + nums[j] + nums[k];
if(sum == 0){
result.add(Arrays.asList(nums[i],nums[j],nums[k]));
while(j < k && nums[j] == nums[j+1]){
j++;
}
while(j < k && nums[k] == nums[k-1]){
k--;
}
j++;
k--;
}
else if(sum > 0){
k--;
}
else{
j++;
}
}
}
return result;
}
}
2.用的是双指针,i固定a,用j和k去确定b和c。不要忘记要sort排序。然后考虑清楚各种去重的情况就行。
18.四数之和
题目
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
代码(算法去重)
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>(); //保存结果集
Arrays.sort(nums); //递增排序
//遍历a,a最多指向倒数第四个元素
for(int i = 0; i < nums.length - 3; i++){
//对a去重,如果当前迭代轮的a和前一轮一样,这一轮直接跳过
if(i > 0 && nums[i] == nums[i - 1]){
continue;
}
//遍历b,b最多指向倒数第三个元素
for(int j = i + 1; j < nums.length - 2; j++){
//对b去重,如果当前迭代轮的b和前一轮一样,也跳过
if(j > i + 1 && nums[j] == nums[j - 1]){
continue;
}
int left = j + 1;
int right = nums.length - 1;
while(left < right){
//错误写法:int sum = nums[i] + nums[j] + nums[left] + nums[right]; 这里int求和会越界
Long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
if(sum > target){
right--;
}
else if(sum < target){
left++;
}
else{
result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while(left < right && nums[right] == nums[right - 1]){
right--;
}
while(left < right && nums[left] == nums[left + 1]){
left++;
}
left++;
right--;
}
}
}
}
return result;
}
}
代码(set去重通过版)
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
Set<List<Integer>> set = new HashSet<>();
for(int i=0; i < nums.length - 3; i++){
for(int j=i+1; j < nums.length - 2; j++){
int k = j + 1;
int l = nums.length - 1;
while(k < l){
long sum = (long)nums[i] + nums[j] + nums[k] + nums[l];
if(sum == target){
set.add(Stream.of(nums[i], nums[j], nums[k], nums[l]).collect(Collectors.toList()));
k++;
l--;
}
else if(sum > target){
l--;
}
else{
k++;
}
}
}
}
List<List<Integer>> res = set.stream().collect(Collectors.toList());
return res;
}
}
总结
1.原理
(1)为什么不能用哈希表
如果使用哈希表,需要用两个for循环变量nums,用map型的hash表存储已经遍历过的a和b,key表示ab之和,value存储单独的a和b的值。每次遍历计算a+b,,然后到map中寻找是否有key=-(a+b)的情况。
和三数之和类似,这里也有两个问题。
第一,map的key是唯一的,只能存储一种a+b之和的key,而对应的所有a,b的单独值的情况不能存储。第二,要求返回的四元组是不重复的,这样的hash表结构无法保证无重复
(2)双指针算法流程
首先对nums数字进行升序排序。
然后用第一层for循环遍历nums,i指向元素a,用第二次for循环遍历nums,j指向元素b。相当于两层for循环先把a和b的值固定了,然后在[j+1,length-1]区间寻找所有满足条件的c和d。
那怎么找c和d呢?我们令left=j+1,指代c,令right=nums.length-1,指代d。当left<right时,计算sum = a+b+c+d,同理,sum大于target,就让right--,sum小于target,就让left--,sum等于target,说明ab固定时找到符合条件的c和d了,把abcd加入结果列表中。
(3)如何去重
如何去a去重,就是在进入第一层for循环后,先判断nums[i]和nums[i-1]的值是否一样,如果一样,用continue跳出当前for循环。因为当索引=i,a固定时,我们会在[i+1,length-1]区间里边找所有满足条件的bcd的情况。而在上一轮索引=i-1时,我们会在[i,length-1]区间里边找所有满足条件的bcd的情况,这里的情况肯定能把下一轮包括进去,所以可以直接continue。注意,要求i要大于0。(这里和三数之和类似)
如何对b去重,就是在进入第二次for循环后,先判断nums[j]和nums[j-1]的值是否一样,如果一样,用continue跳出当前for循环。原理和对a去重一致,不过注意j的下标从i+1开始,所以这里要求j要大于i+1,即第一个j不需要判断去重。
如何对c和d去重,原理和三数之和完全一样。相当于此时cd的区间是[j+1,nums.length-1],在次区间找如何要求的c和d。就是当left和right正好指向符合要求的c和d,即a+b+c+d==0时,我们会把当前的情况add进结果。然后我们要循环判断如果nums[left]==nums[left+1],要让left++,如果nums[right]==nums[right-1],要让right++,也就是把c和d出现相同值的情况给跳过。
2.和三数之和有什么一样
相比于三数之和,只有一层for循环,只用i固定a。这里多加了一层for循环,用j对b进行固定。然后在区间[j+1,nums.length-1]找符合要求的cd。
因为这里四数之和==target而不是0,所以如果nums[i] > 0,不能直接return result进行剪枝了。比如target=-10,可能a=-4,b=-3,c=-2,d=-1。
3.优化点
(1)对a的遍历终止条件是i < nums.length - 3,因为a最多指向倒数第四个元素
(2)对b的遍历终止条件是j < nums.length - 2,因为a最多指向倒数第三个元素
4.注意点
(1)对a去重时:i > 0 && nums[i] == nums[i - 1],有i > 0的条件不能漏掉,因为第一个a是不需要去重判断的
(2)对b去重时:j > i + 1 && nums[j] == nums[j - 1],有j > i + 1的条件不能漏掉,而且j的初始值是i+1而不是0。
(3)特别注意,这里四数之和计算的sum一定要用long型,不然会越界:Long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
第二次刷题总结
1.有一个很栓q的点,因为四数之和sum可能会越界,需要用long型进行存储,(long)nums[i] + nums[j] + nums[l] + nums[r],但是如果写成(long)(nums[i] + nums[j] + nums[l] + nums[r])居然会通过不了。
核心原因是:因为如果后边加了括号,括号内优先计算,四个int先相加,就直接越界了。而后边不加括号,是先把nums[i]先转为long,然后后边int相加会自动提升为long,就不会越界了。
下面附上chatgpt对这个强制转换的解释,说的挺清楚的。