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

454.四数相加II

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。

为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。

例如:
输入:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]
输出:
2
解释:
两个元组如下:

  1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
  2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

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

题目链接/文章讲解/视频讲解:https://programmercarl.com/0454.%E5%9B%9B%E6%95%B0%E7%9B%B8%E5%8A%A0II.html

为什么使用hashmap来实现?
①高效查找
hashmap提供了常数时间的查找(即时间复杂度为O(1))使用哈希表可以显著减少查找操作的时间。
②避免重复计算
使用哈希表可以事先计算出
𝐴[𝑖]+𝐵[𝑗] 的所有组合,并将其存储下来,然后在查找 C[k]+D[l] 时可以直接用 O(1) 时间进行查询。
③ 简化代码实现
利用哈希表将问题分解为两个部分:先计算 A[i]+B[j] 的和,然后存入哈希表中,接着计算 C[k]+D[l] 时,直接查找哈希表中是否存在对应的值。

Map<Integer, Integer> 用来存储和计数。**具体来说:

  • key:A[i] + B[j] 的和
    在哈希表中,key 代表了所有可能的 A[i]+B[j] 的和。也就是说,key 是 A[i]+B[j] 的每个结果。我们通过哈希表的 put() 方法将这些和作为 key 存储。
  • value:A[i] + B[j] 的和出现的次数
    value 表示某个特定和的出现次数。因为 A[i]+B[j] 可能有多个组合的和相等,所以我们需要记录每个和出现的次数。在哈希表中,value 记录的是该和出现的次数。
import java.util.HashMap;
import java.util.Map;
//引入 Java 的 HashMap 类和 Map 接口。
//HashMap 是一个基于哈希表的实现,它提供了高效的键值对存储和查找操作,而 Map 是 HashMap 的接口类型。


public class FourSumCount {
    
    public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
        // 使用HashMap来存储 A[i] + B[j] 的和及其出现次数
        Map<Integer, Integer> sumAB = new HashMap<>();//创建一个哈希表 sumAB,键是A[i]+B[j] 的和,值是该和出现的次数。
        
        // 计算所有 A[i] + B[j] 的和,并记录出现次数
        for (int a : A) {
            for (int b : B) {//两个嵌套的 for 循环遍历数组 A 和 B 中的每一对元素 A[i] 和 B[j]。
                sumAB.put(a + b, sumAB.getOrDefault(a + b, 0) + 1);
            }
        }
        
        int count = 0;
        
        // 对于每个 C[k] + D[l],查找是否存在 -(C[k] + D[l]) 在 sumAB 中
        for (int c : C) {
            for (int d : D) {
                // 查找 sumAB 中是否有 - (c + d) 的键
                count += sumAB.getOrDefault(-(c + d), 0);
            }
        }
        
        return count;
    }
    
    public static void main(String[] args) {
        FourSumCount solution = new FourSumCount();
        
        // 测试数据
        int[] A = {1, 2};
        int[] B = {-2, -1};
        int[] C = {-1, 2};
        int[] D = {0, 2};
        
        int result = solution.fourSumCount(A, B, C, D);
        System.out.println(result);  // 输出结果
    }
}

 sumAB.put(a + b, sumAB.getOrDefault(a + b, 0) + 1);//将计算出来的和 a+b 存储到哈希表 sumAB 中,并记录它出现的次数。
  • put(key, value) 是 HashMap 类的一个方法, 用于将一个键(key)与其对应的值(value)存入哈希表。如果哈希表中已经存在该键,put 会更新该键对应的值;如果该键不存在,则会将该键值对加入哈希表。
  • 在这里,key 是 a + b(即数组 A 和 B 中任意元素的和),value 是该和出现的次数。
 sumAB.getOrDefault(a + b, 0)
  • getOrDefault 方法会先检查哈希表中是否存在键 a + b:如果存在,返回对应的值,即该和出现的次数。如果不存在,返回默认值 0,表示该和尚未出现过。
  • 这个方法的作用是确保在存储和之前的和时,不会抛出 NullPointerException 错误,即使这个和是第一次出现。
for (int c : C) {
    for (int d : D) {
        count += sumAB.getOrDefault(-(c + d), 0);
    }
}
  • 两个嵌套的 for 循环遍历数组 C 和 D 中的每一对元素 C[k] 和 D[l]。计算每对元素的和。
  • 使用 sumAB.getOrDefault(-(c + d), 0) 查找哈希表中是否存在与 −(C[k]+D[l]) 相等的和:将返回的次数加到 count 中,这表示找到了多少个符合条件的元组。如果没有,返回 0。

383. 赎金信

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

题目链接/文章讲解:https://programmercarl.com/0383.%E8%B5%8E%E9%87%91%E4%BF%A1.html

在这里插入图片描述
本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成,但是这里需要注意两点。

  • 第一点“为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思” 这里说明杂志里面的字母不可重复使用。
  • 第二点 “你可以假设两个字符串均只含有小写字母。” 说明只有小写字母,这一点很重要

因为题目说只有小写字母,那可以采用空间换取时间的哈希策略,用一个长度为26的数组记录magazine里字母出现的次数
然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
依然是数组在哈希法中的应用。
一些同学可能想,用数组干啥,都用map完事了,其实在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!

class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        // shortcut
        if (ransomNote.length() > magazine.length()) {
            return false;
        }
        // 定义一个哈希映射数组,用于记录 magazine 中每个字符出现的次数。因为题目中只涉及小写字母('a' 到 'z'),所以数组大小为 26。
        int[] record = new int[26];

        // 遍历 magazine,更新字符频率:
        for(char c : magazine.toCharArray()){
            record[c - 'a'] += 1;
            //每遇到一个字符,就在 record 数组中对应位置的值加 1,表示该字符在 magazine 中出现的次数。
        }

		//遍历 ransomNote,减少字符频率:
        for(char c : ransomNote.toCharArray()){
            record[c - 'a'] -= 1;//对于每个字符 c,我们从 record 数组中对应位置的值减 1,表示从 magazine 中“取走”了一个字符。
        }
        
        // 如果数组中存在负数,说明ransomNote字符串中存在magazine中没有的字符
        for(int i : record){
            if(i < 0){
                return false;
            }
        }

        return true;
    }
}

时间复杂度:
遍历 magazine 和 ransomNote 需要 O(N) 的时间,其中 N 是字符串的长度。

空间复杂度:
使用了一个大小为 26 的数组 record 来记录字符的频率,因此空间复杂度是 O(1)(常数空间),因为 record 数组的大小是固定的。

这道题为什么考虑用数组,不用map?
题目中涉及的字符范围是固定的,即只涉及小写字母 a 到 z,总共有 26 个字符。因此,使用大小为 26 的数组来记录字符的频率是一个非常合适的选择,因为每个字符都可以映射到一个固定的数组索引(a 对应索引 0,b 对应索引 1,依此类推)。
如果使用 Map(如 HashMap)来记录字符频率,我们需要使用字符作为键(char),并且需要将每个字符映射到一个整型值(表示字符的频率)。Map 会为每个键(字符)分配内存空间,此外它还会有额外的结构开销,比如散列表的存储开销。虽然 Map 在平均情况下提供 O(1) 的访问时间,但在最坏情况下(例如哈希冲突严重时),Map 的访问时间可能退化为 O(n)。

15. 三数之和

在这里插入图片描述

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

题目链接/文章讲解/视频讲解:https://programmercarl.com/0015.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.html
在这里插入图片描述

哈希解法

两层for循环就可以确定 a 和b 的数值了,可以使用哈希法来确定 0-(a+b) 是否在 数组里出现过,其实这个思路是正确的,但是我们有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组。
把符合条件的三元组放进vector中,然后再去重,这样是非常费时的,很容易超时,也是这道题目通过率如此之低的根源所在。
去重的过程不好处理,有很多小细节,如果在面试中很难想到位。
时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。
用哈希法需要考虑去重操作,比较麻烦

使用排序+双指针法

首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。
接下来如何移动left 和right呢?
如果nums[i] + nums[left] + nums[right] > 0,说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0, 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
时间复杂度:O(n^2)。
== 考虑去重!==

主要考虑三个数的去重。 a, b ,c, 对应的就是 nums[i],nums[left],nums[right]

a 如果重复了怎么办?
a是nums里遍历的元素,那么应该直接跳过去。

但这里有一个问题,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同。都是和 nums[i]进行比较,是比较它的前一个,还是比较它的后一个。

如果我们的写法是 这样:

if (nums[i] == nums[i + 1]) { // 去重操作
    continue;
}

那我们就把 三元组中出现重复元素的情况直接pass掉了。 例如{-1, -1 ,2} 这组数据,当遍历到第一个-1 的时候,判断 下一个也是-1,那这组数据就pass了。
我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!
所以这里是有两个重复的维度。

那么应该这么写:

if (i > 0 && nums[i] == nums[i - 1]) {
    continue;
}

这么写就是当前使用 nums[i],我们判断前一位是不是一样的元素,在看 {-1, -1 ,2} 这组数据,当遍历到 第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。

b与c的去重
去重的逻辑要多加对right 和left 的去重:

while (right > left) {
    if (nums[i] + nums[left] + nums[right] > 0) {
        right--;
        // 去重 right
        while (left < right && nums[right] == nums[right + 1]) right--;
    } else if (nums[i] + nums[left] + nums[right] < 0) {
        left++;
        // 去重 left
        while (left < right && nums[left] == nums[left - 1]) left++;
    } else {
    }
}

在这里插入图片描述

18. 四数之和

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

题目链接/文章讲解/视频讲解:https://programmercarl.com/0018.%E5%9B%9B%E6%95%B0%E4%B9%8B%E5%92%8C.html

在这里插入图片描述

import java.util.*;

public class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        Arrays.sort(nums);  // 排序数组
        List<List<Integer>> result = new ArrayList<>();  // 结果集
        for (int k = 0; k < nums.length; k++) {
            // 剪枝处理
            if (nums[k] > target && nums[k] >= 0) {
                break;
            }
            // 对nums[k]去重
            if (k > 0 && nums[k] == nums[k - 1]) {
                continue;
            }
            for (int i = k + 1; i < nums.length; i++) {
                // 第二级剪枝
                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.length - 1;
                while (right > left) {
                    long sum = (long) nums[k] + nums[i] + nums[left] + nums[right];
                    if (sum > target) {
                        right--;
                    } else if (sum < target) {
                        left++;
                    } else {
                        result.add(Arrays.asList(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;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        int[] nums = {1, 0, -1, 0, -2, 2};
        int target = 0;
        List<List<Integer>> results = solution.fourSum(nums, target);
        for (List<Integer> result : results) {
            System.out.println(result);
        }
    }
}

四数之和,——>使用双指针法

不要判断nums[k] > target 就返回了,三数之和 可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。
比如:数组是[-4, -3, -2, -1],target是-10,不能因为-4 > -10而跳过。
但是我们依旧可以去做剪枝,逻辑变成nums[i] > target && (nums[i] >=0 || target >= 0)就可以了。

1. fourSum 方法:

public List<List<Integer>> fourSum(int[] nums, int target) {
    Arrays.sort(nums);  // 排序数组
    List<List<Integer>> result = new ArrayList<>();  // 结果集

Arrays.sort(nums); 首先对输入数组 nums 进行排序。这一步非常关键,因为排序后的数组使得后续的双指针方法能够高效地工作。
List<List> result = new ArrayList<>();:创建一个结果集 result,用于存储最终满足条件的四元组。

2. 外层循环:

for (int k = 0; k < nums.length; k++) {
    // 剪枝处理
    if (nums[k] > target && nums[k] >= 0) {
        break;
    }
    // 对nums[k]去重
    if (k > 0 && nums[k] == nums[k - 1]) {
        continue;
    }
  • 外层循环的作用:k 是用来固定第一个数字的索引。循环遍历整个数组,通过 nums[k] 来确定第一个数。
  • 剪枝处理:在某些情况下,如果当前数已经大于目标值并且是正数(因为数组已经排序,后面的数更大),那么就可以提前结束循环,避免不必要的计算。这通过if (nums[k] > target && nums[k] >= 0) 来实现。
  • 去重:为了避免重复的四元组,在每次选择nums[k] 时,检查当前的 nums[k] 是否与前一个数相同。如果相同,就跳过当前数,避免重复计算。

3. 第二层循环:

for (int i = k + 1; i < nums.length; i++) {
    // 第二级剪枝
    if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
        break;
    }
    // 对nums[i]去重
    if (i > k + 1 && nums[i] == nums[i - 1]) {
        continue;
    }

  • 第二层循环的作用:i 是用来固定第二个数字的索引。此循环从 k + 1 开始,避免选择重复的数。
  • 剪枝处理:在选择第二个数 nums[i] 时,如果当前 nums[k] + nums[i] 已经大于目标值并且是正数(后续的数值会更大),就可以提前结束循环
  • 去重:同样地,如果当前的 nums[i] 与前一个数 nums[i - 1] 相同,则跳过当前的 i,避免重复。

4. 双指针:

int left = i + 1;
int right = nums.length - 1;
while (right > left) {
    long sum = (long) nums[k] + nums[i] + nums[left] + nums[right];
    if (sum > target) {
        right--;
    } else if (sum < target) {
        left++;
    } else {
        result.add(Arrays.asList(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++;
    }
}

双指针的作用:在确定了前两个数字 nums[k] 和 nums[i] 后,通过使用两个指针 left 和 right 来遍历数组的剩余部分,寻找满足条件的第三个和第四个数。

  • left 从 i + 1 开始,right 从数组的最后一个元素开始。
  • sum 是当前四个数之和:nums[k] + nums[i] + nums[left] + nums[right]。
  • 如果 sum 大于目标值,则移动 right 指针向左缩小总和;如果 sum 小于目标值,则移动 left 指针向右增大总和。
  • 如果 sum 等于目标值,则将当前四元组添加到结果集 result 中,并继续移动 left 和 right 指针,同时避免重复。

5. 去重处理:

在添加四元组后,通过 while (right > left && nums[right] == nums[right - 1]) right–; 和 while (right > left && nums[left] == nums[left + 1]) left++; 来跳过重复的数,确保结果集中的四元组唯一。

6. 返回结果:

return result;

最后,返回包含所有满足条件的四元组的 result 列表。

7.main方法

public static void main(String[] args) {
    Solution solution = new Solution();
    int[] nums = {1, 0, -1, 0, -2, 2};
    int target = 0;
    List<List<Integer>> results = solution.fourSum(nums, target);
    for (List<Integer> result : results) {
        System.out.println(result);
    }
}

main 方法用于测试 fourSum 方法。给定数组 nums = {1, 0, -1, 0, -2, 2} 和目标值 target = 0,调用 fourSum 方法并打印返回的所有四元组。

时间复杂度:排序的时间复杂度为 O(n log n),外层和内层循环的时间复杂度分别为 O(n),双指针部分的时间复杂度为 O(n)。因此,总的时间复杂度为 O(n^3)。

空间复杂度:主要是结果集的空间,最坏情况下空间复杂度为 O(n^3),不过实际情况下远小于这个值。

剪枝优化:通过提前退出循环和去重,有效减少了不必要的计算。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值