代码随想录第七天 | Leecode 454.四数相加II 、383. 赎金信 、15. 三数之和 、18. 四数之和

Leecode 454. 四数相加II

题目描述

给你四个整数数组 nums1nums2nums3nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
  1. 示例 1:
  • 输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
  • 输出:2
  • 解释:
    • 两个元组如下:
      1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
      2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
  1. 示例 2:
  • 输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
  • 输出:1

解题思路与代码展示

考虑使用unordered_map容器,首先统计前两个数组求和得到的每个结果的数量,再对后两个数组求和,并取和的负数在容器中已经统计的数量求和。这样即可计算得到四个数组四数之和为0的数量。具体代码如下:

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int,int> map; // 初始化哈希映射键值对
        int count = 0;  // 初始化满足条件的四数之和的数量为0
        for(int num1 : nums1){
            for(int num2 : nums2){
                map[num1 + num2]++; // 计算前两个数组中求和后的结果并统计数量,将每个出现的求和作为键将其对应的值+1
            }
        }
        for(int num3 : nums3){
            for(int num4 : nums4){
                if(map.find(-(num3+num4)) != map.end()) // 如果后两个数组求和结果的负值作为键能够在map容器中找到对应的值
                      count += map[-(num3+num4)];  // 则将该值进行累加计数
            }
        }
        return count;
    }
};

使用上面代码,即可计算得到四数之和为0的组合数量。分析上面代码,由于题目已经给定说明四个数组的长度都是\(n\),那么可以推算时间复杂度为\(O(n^2)\).

Leecode 383. 赎金信

题目描述

给你两个字符串:ransomNotemagazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false

magazine 中的每个字符只能在 ransomNote 中使用一次。

  • 示例 1:
    • 输入:ransomNote = "a", magazine = "b"
    • 输出:false
  • 示例 2:
    • 输入:ransomNote = "aa", magazine = "ab"
    • 输出:false
  • 示例 3:
    • 输入:ransomNote = "aa", magazine = "aab"
    • 输出:true

解题思路及代码展示

本题需要判断能否用字符串magazine中的字符组成ransomNote中的字符,一开始乍一看没反应过来这两个字符串的长度大小关系。后来突然注意到本题的标题“赎金信”对应字符串ransomNote,而字符串magazine表示杂志。只要联想场景绑匪为了不暴露自己的字迹,通过剪下杂志字符串中的字符来拼凑组成赎金信。那么这种情况下每个字符只能使用一次,即字符串ransomNote中的字符应该是真包含于字符串magazine中的。同时又需要注意这里并非表示集合,因为字符串中的字符是可以重复多次出现的。

在理解了题目之后,我们来思考如何解决这个问题。其实如果代入“绑匪”视角,为了判断能否组成赎金信,当然首先整理并统计一下目前杂志中每个字符都出现了多少次。随后根据赎金信中所需要用到字符逐一取出并使用。如果在需要某个字符的时候,整理统计好的字符中已经全部用完,那么则说明字符串不够。如果最终顺利组装完成了赎金信,则说明可以完成。

那么根据上面思想,我们可以写出下面代码:

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        unordered_map<char,int> umap; // 初始化map键值对,可以根据字符类型的键在O(1)时间内搜索到存储的对应的int类型的值
        for(int i = 0; i < magazine.size(); i++){ // 遍历杂志中所有的字符
            umap[magazine[i]]++;  // 将统计所有的字符,每个字符对应的数量+1
        }
        for(int i = 0; i < ransomNote.size(); i++){ // 遍历赎金信
            if(umap[ransomNote[i]]-- <= 0) return false; // 赎金信每用一个字符就将统计的键值对中对应的值-1;如果此时值小于等于0,则说明不够,返回false
        }
        return true; // 如果遍历完赎金信都还没有出现字符不够的情况,则说明可以组成赎金信,返回true
    }
};

上面代码可以看出,时间复杂度在\(O(m+n)\),即将两个字符串各遍历一遍即可完成这个算法。

Leecode 15. 三数之和

题目描述

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

  • 示例 1:
    • 输入:nums = [-1,0,1,2,-1,-4]
    • 输出:[[-1,-1,2],[-1,0,1]]
    • 解释:
      • nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0
      • nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0
      • nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0
      • 不同的三元组是 [-1,0,1][-1,-1,2]
      • 注意,输出的顺序和三元组的顺序并不重要。
  • 示例 2:
    • 输入:nums = [0,1,1]
    • 输出:[]
    • 解释:唯一可能的三元组和不为 0
  • 示例 3:
    • 输入:nums = [0,0,0]
    • 输出:[[0,0,0]]
    • 解释:唯一可能的三元组和为 0

解法一,暴力求解

首先是一个使用四层for循环的,很纯粹的暴力解。即先对数组进行排序,再遍历三个指针的所有可能的不重复的取值,如果三个值求和相等则取出。取出之后再对已经找到的解矩阵中使用一层for循环来判断当前数组是否重复。如果重复则丢弃,不重复则放入当前结果矩阵中。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(),nums.end()); // 先进行一次快排
        for(int i = 0; i < nums.size()-2; i++){ // 第一个指针i取值从0到长度-3
            for(int j = i + 1; j < nums.size()-1; j++){ // 第二个指针 j 取值从 i + 1到长度-2
                for(int k = j + 1; k < nums.size(); k++){ // 第三个指针 k 取值从 j + 1到长度-1
                    if(nums[i]+ nums[j] + nums[k] == 0) { // 如果三数之和满足条件
                        bool exist = false; // 初始化当前值是否重复的布尔变量
                        vector<int> newVec({nums[i], nums[j], nums[k]}); // 先将当前三个值组成vector数组
                        for(int l = 0; l < result.size(); l++){  // 逐一判断已经找到的解集,是否已经有当前找到的vector
                            if(newVec == result[l]) exist = true; // 如果已经找到,则更改exist为true
                        }
                        if(exist) continue; // 如果当前vector已经找到,则直接继续循环查找其他值
                        result.push_back(newVec); // 如果当前vector并不存在与解集中,则将其放入解集
                    }
                }
            }
        }
        return result;
    }
};

上面算法最差情况下的时间复杂度为\(O(n^4)\),最好情况也有\(O(n^3)\)。都是非常高的时间复杂度。而且该算法也不能通过Leecode网站的所有测试用例(因为会超时)。因此我们再考虑使用更高效的算法。

解法二,双指针

双指针法根据已知过大还是过小的信息来确定指针查找的方向,以此来降低搜索满足条件的解的算法的时间复杂度。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result; // 初始化解集
        sort(nums.begin(),nums.end()); // 对输入的vector进行排序
        for(int a = 0; a < nums.size() - 2; a++){ // 遍历第一个指针a可能取到从0到n-3
            if(a > 0 && nums[a] == nums[a-1]) continue;  
                    // 除了当前a在初始起点的时候之外,如果当前值和上一个值相同,说明后续找到的另外两个指针对应的值也相同,故直接跳过
            
            int left = a + 1; // left指针起始值从当前a的下一个开始开始往右搜索
            int right = nums.size() - 1; // right指针从最右侧往左搜索

            while(left < right){  // 当left指针和right指针还未相碰时,则一直进行循环
                int sum2 = nums[left] + nums[right]; // 对后两个指针对应的值求和
                if( sum2 == -nums[a]) { // 如果求和的值是a对应的值的相反数,则说明求和为0
                    result.push_back(vector<int>({nums[a], nums[left++], nums[right-- ]})); // 则将当前数组记录,同时左右指针都需要移动一位
                    while(left < nums.size() && nums[left] == nums[left-1]) left++; // 如果左右指针在刚移动了一位之后,当前值和上一步的值还相同,需要一直继续移动
                    while(right > left && nums[right] == nums[right+1]) right--; // 直至左右指针都找到一个新的取值时再进入下一次循环
                }
                else if(sum2 > -nums[a]) right--; // 如果当前三个值求和大于0,说明right指针需要减小
                else left++; // 如果当前三个值求和小于0,说明left指针需要变大
            }
        }
        return result;
    }
};

使用上面算法,即可在去重的前提下,找到所有的三数之和为0的组合。时间复杂度为\(O(n^2)\).

Leecode 18. 四数之和

题目描述

  • 给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n

  • abcd 互不相同

  • nums[a] + nums[b] + nums[c] + nums[d] == target
    你可以按 任意顺序 返回答案 。

  • 示例 1:

    • 输入:nums = [1,0,-1,0,-2,2], target = 0
    • 输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
  • 示例 2:

    • 输入:nums = [2,2,2,2,2], target = 8
    • 输出:[[2,2,2,2]]

解题思路与代码展示

本题思路和上一题非常类似,都是使用双指针,同时根据当前求和偏大还是偏小来相应地调整两个指针的搜索方向。以此来减少所需要遍历的可能解的数量。但和上一题的区别在于,需要多加一层循环,以此来表示多一个指针的可能取值。

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> result; // 定义result作为解集存放所有解
        if(nums.size() < 4) return result; // 如果当前长度小于4,则四数之和无意义,直接返回result
        sort(nums.begin(),nums.end());  // 对vector容器进行排序
        for(int a = 0; a < nums.size() - 3; a++){ // 第一个指针a的取值从0到数组长度n-4
            if(a != 0 && nums[a] == nums[a-1]) continue;  // 避免重复查找,如果第一个指针在移动过后,和上一次的数值还相同,则需要继续移动
            for(int b = a+1; b < nums.size() - 2; b++){ // 第二个指针b的取值从a+1到数组长度n-3
                if(b != a+1 && nums[b] == nums[b-1]) continue; // 同样,如果指针b在移动过后,和上一次的数组还相同,则需要继续移动
                int left = b + 1; // 左指针可取的最小值为b+1
                int right = nums.size()-1; // 右指针可取值为数组中最后一位,即长度n-1
                while(left < right){ // 只要左右指针还未相碰,则一直遍历搜索
                    long sum = (long)nums[a] + (long)nums[b] + (long)nums[left] + (long)nums[right]; // 计算四数之后,此时int类型会溢出,因此转换为long类型
                    if(sum == target){ // 如果求和满足条件
                        result.push_back(vector<int>({nums[a],nums[b],nums[left++],nums[right--]})); // 将找到的数组成数组存放入解集,并同时移动left和right指针
                        while(left < right && nums[right] == nums[right+1]) right--; // 如果左右指针移动过后,取值还和刚才相等,需要一直移动到值不相等
                        while(left < right && nums[left] == nums[left-1]) left++;
                    }
                    else if(sum > target) right--; // 如果四数之和太大,则需要令right指针减小
                    else left++; // 如果四数之后太小,则需要让left指针变大
                }
            }
        }
        return result;
    }
};

本题上面算法之比上一题多了一层for循环,其余过程都非常相近。因此可以知道时间复杂度为\(O(n^3)\).

今日总结

今天继续学习了哈希表部分的题目,更进一步熟悉了unordered_set容器和unordered_map容器。

posted on 2025-04-02 02:03  JQ_Luke  阅读(1169)  评论(0)    收藏  举报