算法学习打卡day6|454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和

454.四数相加II

力扣题目链接
题目描述:
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

示例 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

思路:

  • 联想两数相加时,采用了哈希表,将一个数组num1的元素添加到哈希map中,然后在其中查找target - nums2[i]是否存在,存在即为所求组。
  • 四数相加类似,我们可以将nums1和nums2的和sum1存到哈希map中,因为要求返回元组数目,所以,value值为和个数。然后双层循环遍历nums3和nums4,求这两的和sum2,从mao中查找target - sum2是否存在,这时就转换为两数求和。
  • 注意:结果集加的是map中对应的value值,而不是简单的++
  • 为什么两两求和而不是一个数组存到map,用另外三个数组来判断?
    • 因为三个数组是时间复杂度变为O(n^3)。 而两两求和为O(n^2)。

解题步骤:

  • 首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
  • 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
  • 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
  • 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。最后返回统计值 count 就可以了。

代码实现:

int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int, int> map;
        int result = 0;
        int sum1 = 0, sum2 = 0, n = nums1.size();
        for(int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                sum1 = nums1[i] + nums2[j];
                    map[sum1]++;
            }
        }
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                sum2 = nums3[i] + nums4[j];
                if (map.count(0 - sum2)) {
                    result += map[0 - sum2];
                }
            }
        }
        return result;
    }

383. 赎金信

力扣题目链接
题目描述:
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。

示例 1:
输入:ransomNote = “a”, magazine = “b”
输出:false

示例 2:
输入:ransomNote = “aa”, magazine = “ab”
输出:false

做题时长:8分20秒

思路:

  • 一看题发现是两个字符串,一个字符串从另一个字符串里查找是否存在,这个题和 *242.有效的字母异位词*类似,有效字母异位词是判断两个字符串的字符是否完全相同,这个题是判断一个是否包含另一个,因为是26个字母,那么,同样可以采用哈希数组解决。
  • 为什么不用map?
    • 因为map底层由红黑树或哈希表实现,还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!

代码实现:

bool canConstruct(string ransomNote, string magazine) {
        vector<int> a(26, 0);
        for (char& i : magazine) {
            a[i - 'a']++;
        }
        for (char& i : ransomNote) {
            if (--a[i - 'a'] < 0) {
                return false;
            }
        }
        return true;
    }

15. 三数之和

力扣题目链接
题目描述:
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != 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] 。
注意,输出的顺序和三元组的顺序并不重要。

思路:

  • 这个题以及四数之和,和第一题有所差别,因为是从同一个数组里求三数之和且不能采用相同元素,故采用哈希表的方法十分复杂,可以采用双指针法。

  • 首先用一个变量 i 去遍历数组,然后left指向 i + 1,right指向最后一个元素,当三个值求和大于target,就right–,小于就left++,相等时即为所求元组
    算法学习打卡day6|454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和_第1张图片

  • 如何去重?

    • 我们在每一趟时都要对三个元素进行去重,对于a,只要它和前一个元素相同就跳过。
    • 注意:如果我们的写法是 这样:
      if (nums[i] == nums[i + 1]) { // 去重操作 continue; }
      那我们就把 三元组中出现重复元素的情况直接pass掉了。 例如{-1, -1 ,2} 这组数据,当遍历到第一个-1 的时候,判断 下一个也是-1,那这组数据就pass了。
    • 对于left和right,直接看代码:
    • while(left < right && nums[left] == nums[left + 1]) { left++; }
      while(left < right && nums[right] == nums[right - 1]) { right--; }

代码实现

vector<vector<int>> results;
        sort(nums.begin(), nums.end());
        int left = 0, right = 0;
        for(int i = 0; i < nums.size() - 2; ++i) {
            if (nums[i] > 0) {
                return results;
            }
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            left = i + 1, right = nums.size() - 1;
            while(left < right) {
                if (nums[i] + nums[left] + nums[right] > 0) {
                    right--;
                } else if (nums[i] + nums[left] + nums[right] < 0) {
                    left++;
                } else {
                    results.push_back({nums[i], nums[left], nums[right]});
                    while(left < right && nums[left] == nums[left + 1]) {
                        left++;
                    }
                    while(left < right && nums[right] == nums[right - 1]) {
                        right--;
                    }
                    left++;
                    right--;
                }
            }
        }
        return results;
    }   

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
你可以按 任意顺序 返回答案 。

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

  • 四数之和和三数之和是一个思路,只不过是先定两个数a和b,然后剩下两个数用双指针解决,同理五数之和、六数之和都可以这么做。
  • 注意:
    • 剪枝有些区别,不再是nums[i] > 0,因为有负数的存在,因此外层剪枝需要改为 nums[i] > target && (nums[i] >= 0 || target >= 0),内层剪枝需要改为 nums[j] + nums[i] > target && (nums[j] + nums[i] >= 0 || target >= 0)
    • 内层循环剪枝不是return,而是break到外层。
    • 还有注意四数求和的上溢问题,需要将类型改为long
      代码实现
vector<vector<int>> results;
        if (nums.size() < 4) {
            return results;
        }
        sort(nums.begin(), nums.end());
        int left = 0, right = 0;
        for (int i = 0; i < nums.size() - 3; ++i) {
            if (nums[i] > target && (nums[i] >= 0 || target >= 0)) {
                return results;
            }
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            for (int j = i + 1; j < nums.size() - 2; ++j) {
                if (nums[j] + nums[i] > target && (nums[j] + nums[i] >= 0 || target >= 0)) {
                    break;
                }
                if (j > i + 1 && nums[j] == nums[j - 1]) {
                    continue;
                }
                left = j + 1, right = nums.size() - 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 {
                        results.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});
                        while(left < right && nums[left] == nums[left + 1]) {
                            left++;
                        }     
                        while(left < right && nums[right] == nums[right - 1]) {
                            right--;
                        }
                        left++;
                        right--;
                    }
                }
            }
        }
        return results;
    }

哈希表总结

哈希表的应用场景:

  1. 当需要判断一个元素是否在集合中或者一个元素是否出现过的时候
  2. 当需要去重的时候,要想到用哈希表

哈希函数和哈希碰撞:

  1. 哈希函数是用来计算哈希key值的,用来唯一标识一个数据
  2. 哈希碰撞是指哈希函数计算出的key值相同导致访问冲突,怎么解决? 线性探测法和拉链法线性探测法 需要数组足够大,起码足够容纳数据,拉链法 是将同key的数据用链表链接起来.

哈希数组:

  • 当数据量给定,且基本连续的时候可以使用数组来实现简单哈希,效率更高,通常为字符串操作字符的时候,例题:242.有效的字母异位词 383. 赎金信 ,两道题类似的做法。
    哈希set:

  • 当数据量很大时,不适合使用数组,

    • 主要因为如下两点:
      1. 数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
      2. 如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
  • std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希, 使用unordered_set 读写效率是最高的,不需要对数据进行排序,而且还不要让数据重复,选择unordered_set,例题:两个数组交集快乐数,两题都是有着判断元素是否存在和是否出现过的场景。

  • 一般只需要用到key值,而不用value的时候用set,而用value时则用map。

哈希map:

  • 使用数组和set来做哈希法的局限。
    • 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
    • set是一个集合,里面放的元素只能是一个key,而碰到 两数之和 这种题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
    • 而map是一种的结构,对于两数之和这道题,可以用key保存数值,用value在保存数值所在的下标。所以使用map最为合适。
  • 454.四数相加II,和两数之和类似,但是需要求四个数字的和,故可以话为两两求和,再判断,从而简化为两数之和问题,同样适用unordered_map,key为两数之和,value为出现次数。
  • 三数之和四数之和,咂一看和四数相加差不多,但区别在于这两道题要求在一个数组里找目标元组,这就要求不能重复使用同一元素,采用哈希法会比较困难,这里采用双指针法解决会比较好。

你可能感兴趣的:(算法学习打卡,算法,学习,哈希表)