代码随想录训练营 Day7打卡 哈希表part02 454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和

代码随想录训练营 Day7打卡 哈希表part02

一、 力扣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
解释:
两个元组如下:
  (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
  (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

实现思路:

为了优化这一过程,我们可以将问题分解为两个部分,并利用哈希表来减少必要的计算量:

1. 数组分组:

  • 将四个数组分为两组,例如把 nums1 和 nums2 作为一组,nums3 和 nums4 作为另一组。

2. 计算和存储两数之和:

  • 对第一组的每对元素(来自 nums1 和
    nums2)计算它们的和,并将和的值作为键,出现次数作为值存储在哈希表中。这样,每个和的计算复杂度为 O(n2 )。

  • 存储和的次数是因为相同的和可能由不同的元素对产生,这对于后续的计数是必要的。

3. 匹配和查询:

  • 对于第二组的每对元素(来自 nums3 和 nums4),计算它们的和的相反数,然后在哈希表中查询这个相反数是否存在。

  • 如果存在,说明有相应数量的第一组元素对的和与之匹配,即它们加起来的总和为零。因此,可以直接将哈希表中该和的值(即出现次数)累加到结果中。

为什么使用哈希表

  • 效率:哈希表提供平均时间复杂度为 O(1) 的查找,使得对于第二组元素对的每次查询都非常快速。
  • 简化计算:通过将四重循环降低为两次二重循环,大大减少了计算量。前两重循环用于构建哈希表,后两重循环用于查询,整体复杂度降低到
    O(n2 )。
  • 空间换时间:使用额外的空间(哈希表)来存储中间结果,以换取时间效率的提升。
版本一:使用字典
class Solution(object):
    def fourSumCount(self, nums1, nums2, nums3, nums4):
        # 使用字典存储 nums1 和 nums2 中所有可能的两数之和的频率
        hashmap = dict()
        for n1 in nums1:
            for n2 in nums2:
                # 用 get 方法优化取值和更新字典,如果键不存在,则返回 0 并加 1
                hashmap[n1 + n2] = hashmap.get(n1 + n2, 0) + 1
        
        count = 0
        for n3 in nums3:
            for n4 in nums4:
                key = -n3 - n4
                # 检查 nums3 和 nums4 的和的相反数是否存在于 hashmap 中
                if key in hashmap:
                    # 如果存在,累加这个键对应的值到 count
                    count += hashmap[key]
        return count

版本二:使用 defaultdict
from collections import defaultdict 
class Solution:
    def fourSumCount(self, nums1: list, nums2: list, nums3: list, nums4: list) -> int:
        # 使用 defaultdict 来自动处理不存在的键的情况
        rec, cnt = defaultdict(int), 0
        # 遍历 nums1 和 nums2 并记录两数之和及其出现的次数
        for i in nums1:
            for j in nums2:
                rec[i + j] += 1
        # 遍历 nums3 和 nums4 并查询相反数是否已记录
        for i in nums3:
            for j in nums4:
                cnt += rec.get(-(i + j), 0)
        # 返回匹配到的四元组数量
        return cnt

力扣题目链接
题目文章讲解
题目视频讲解

二、 力扣383. 赎金信

给你两个字符串:ransomNote 和 magazine ,判断 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)。该问题可通过多种方式解决,包括数组、哈希表、以及Python内置的collections库中的数据结构。

版本一:使用数组

由于只涉及小写字母,我们可以使用一个长度为26的数组来记录每个字母的出现次数。

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        # 创建两个长度为26的数组,分别用于记录ransomNote和magazine中各字母的出现次数
        ransom_count = [0] * 26
        magazine_count = [0] * 26
        
        # 计算ransomNote中每个字母的出现次数
        for c in ransomNote:
            ransom_count[ord(c) - ord('a')] += 1
        
        # 计算magazine中每个字母的出现次数
        for c in magazine:
            magazine_count[ord(c) - ord('a')] += 1
        
        # 比较两个数组,确保ransomNote中的每个字母在magazine中都有足够的数量
        return all(ransom_count[i] <= magazine_count[i] for i in range(26))

版本二:使用defaultdict

使用defaultdict来避免显式检查键是否存在于字典中。

from collections import defaultdict

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        # 使用defaultdict记录magazine中每个字符的出现次数
        hashmap = defaultdict(int)
        for x in magazine:
            hashmap[x] += 1
        
        # 检查ransomNote中的每个字符是否都在hashmap中有足够的数量
        for x in ransomNote:
            if hashmap[x] == 0:
                return False
            hashmap[x] -= 1

        return True

版本三:使用字典

普通字典的使用方法,与defaultdict类似,但需要手动检查键是否存在。

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        # 使用普通字典记录magazine中每个字符的出现次数
        counts = {}
        for c in magazine:
            counts[c] = counts.get(c, 0) + 1
        
        # 检查ransomNote中的每个字符是否都在counts中有足够的数量
        for c in ransomNote:
            if c not in counts or counts[c] == 0:
                return False
            counts[c] -= 1
        
        return True

版本四:使用Counter

使用Counter来简化计数过程,并直接比较两个计数器。

from collections import Counter

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        # 使用Counter比较ransomNote和magazine的字母计数
        return not Counter(ransomNote) - Counter(magazine)

版本五和六:使用count方法

直接使用字符串的count()方法来检查每个字符是否有足够的出现次数。

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        # 对ransomNote中的每个唯一字符,确保它在magazine中出现的次数至少与其在ransomNote中的次数一样多
        return all(ransomNote.count(c) <= magazine.count(c) for c in set(ransomNote))

class Solution:
    def canConstruct(self, ransomNote: str, magazine: str) -> bool:
        for char in ransomNote:
            if ransomNote.count(char) > magazine.count(char):
                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] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

算法思想:

在解决三数之和等问题时,双指针法是一种非常有效的解决方案,尤其是当与哈希表方法相比。双指针法可以有效减少不必要的计算和重复的结果,特别是在有序数组中。下面详细解释双指针法的实现思路,以及如何利用哈希表优化去重和查找过程。其动画如下:

在这里插入图片描述
这种方法首先通过排序来帮助快速排除不可能的组合,并通过左右指针的移动来寻找合适的三元组。对于避免重复的三元组,我们在找到一组有效的三元组后,同时跳过左右两边的重复元素。

版本一:使用双指针
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        result = []  # 用于存储结果的列表
        nums.sort()  # 对数组进行排序
        
        for i in range(len(nums)):
            # 如果第一个元素大于0,后面的数都大于0,不可能三数之和为0
            if nums[i] > 0:
                break
            
            # 跳过重复的元素,避免生成重复的三元组
            if i > 0 and nums[i] == nums[i - 1]:
                continue
                
            left, right = i + 1, len(nums) - 1  # 初始化双指针
            
            while right > left:
                sum_ = nums[i] + nums[left] + nums[right]
                
                if sum_ < 0:
                    left += 1  # 三数之和小于0,left指针右移增大总和
                elif sum_ > 0:
                    right -= 1  # 三数之和大于0,right指针左移减小总和
                else:
                    result.append([nums[i], nums[left], nums[right]])
                    
                    # 跳过重复的元素
                    while right > left and nums[right] == nums[right - 1]:
                        right -= 1
                    while right > left and nums[left] == nums[left + 1]:
                        left += 1
                        
                    right -= 1
                    left += 1
                    
        return result

版本二:使用哈希表进行优化
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        result = []
        nums.sort()  # 对数组进行排序
        for i in range(len(nums)):
            if nums[i] > 0:  # 排序后第一个数大于0,后面不可能有三数之和为0
                break
            if i > 0 and nums[i] == nums[i - 1]:  # 跳过重复的元素a
                continue
            d = {}  # 哈希表存储遍历的元素
            for j in range(i + 1, len(nums)):
                if j > i + 2 and nums[j] == nums[j-1] == nums[j-2]:  # 跳过重复的元素b
                    continue
                c = -nums[i] - nums[j]
                if c in d:  # 如果c已存在于哈希表中,说明找到一组解
                    result.append([nums[i], nums[j], c])
                    d.pop(c)  # 删除键c以避免重复
                else:
                    d[nums[j]] = j  # 否则将当前数字存入哈希表
        return result

这个版本通过哈希表来避免多次遍历同样的数字,哈希表存储已经遍历过的数字,从而快速判断组合是否存在。这种方法在处理大数据集时,尤其是数组中有多个重复元素时,可以显著减少运算次数。

总结
双指针法相较于哈希表方法,在处理排序数组时更为高效,尤其是在避免重复结果方面。通过双指针法,可以有效利用数组的有序性,通过逻辑简单的指针移动来找到目标组合,同时避免了哈希表方法中复杂的去重逻辑。这使得双指针法在面试和实际应用中是解决这类问题的首选方法。

力扣题目链接
题目文章讲解
题目视频讲解

四、力扣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]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

基本思路

在解决四数之和问题时,我们借鉴了三数之和问题中双指针法的核心思想,即固定两个数,然后在剩余数组中寻找另外两个数以达到目标和。四数之和问题中,我们同样采用双指针法,但需要在外层循环的基础上再增加一层循环,以固定两个数(而非一个数),之后在剩余数组中寻找另外两个数。

解决方案的细节
  • 双指针的使用:类似于三数之和,四数之和问题中我们固定前两个数nums[k]和nums[i],然后在剩下的数组中使用双指针left和right来寻找满足nums[k] + nums[i] + nums[left] + nums[right] == target的组合。
  • 剪枝优化:虽然不能简单地通过nums[k] > target来终止循环,但我们仍然可以进行剪枝,即当nums[i] > target且nums[i]非负或target非负时,可以提前终止,因为这表明后续的数无法构成目标和。
  • 时间复杂度:四数之和问题的时间复杂度为O(n3),这是因为增加了额外的一层循环来固定第二个数。
版本一:双指针法

这种方法是在排序数组中通过固定两个数,然后使用两个指针分别指向剩余部分的开头和结尾,通过比较和与目标值的大小来移动指针,从而找出所有满足条件的组合。

以下版本只为方便理解,没有加入去重和剪枝:

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()  # 先对数组进行排序
        n = len(nums)
        result = []
        for i in range(n):
            for j in range(i+1, n):
                left, right = j+1, n-1
                while left < right:
                    s = nums[i] + nums[j] + nums[left] + nums[right]
                    if s == target:
                        result.append([nums[i], nums[j], nums[left], nums[right]])
                        # 跳过重复的数字
                        while left < right and nums[left] == nums[left+1]:
                            left += 1
                        while left < right and nums[right] == nums[right-1]:
                            right -= 1
                        left += 1
                        right -= 1
                    elif s < target:
                        left += 1
                    else:
                        right -= 1
        return result

完整版本:

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()  # 先对数组进行排序
        n = len(nums)
        result = []
        for i in range(n):
            # 剪枝:如果当前数字过大,后续不再可能找到合适的组合
            if nums[i] > target and nums[i] >= 0 and target >= 0:
                break
            # 去重:跳过重复的数字以避免重复的结果
            if i > 0 and nums[i] == nums[i-1]:
                continue
            for j in range(i+1, n):
                # 剪枝:如果当前两数之和已经过大,终止这层循环
                if nums[i] + nums[j] > target and target >= 0:
                    break
                # 去重:跳过重复的数字以避免重复的结果
                if j > i+1 and nums[j] == nums[j-1]:
                    continue
                left, right = j+1, n-1
                while left < right:
                    s = nums[i] + nums[j] + nums[left] + nums[right]
                    if s == target:
                        result.append([nums[i], nums[j], nums[left], nums[right]])
                        # 跳过重复的数字
                        while left < right and nums[left] == nums[left+1]:
                            left += 1
                        while left < right and nums[right] == nums[right-1]:
                            right -= 1
                        left += 1
                        right -= 1
                    elif s < target:
                        left += 1
                    else:
                        right -= 1
        return result

版本二:使用字典

该方法通过构建一个字典来记录每个数字出现的频率,然后固定三个数,查找是否存在第四个数使得四数之和等于目标值。

class Solution:
    def fourSum(self, nums, target):
        freq = {}  # 字典记录每个数出现的频率
        for num in nums:
            freq[num] = freq.get(num, 0) + 1
        
        ans = set()  # 使用集合存储答案以避免重复
        nums = sorted(nums)  # 对数字进行排序
        for i in range(len(nums)):
            for j in range(i + 1, len(nums)):
                for k in range(j + 1, len(nums)):
                    val = target - (nums[i] + nums[j] + nums[k])
                    # 检查需要的第四个数字是否在字典中
                    if val in freq:
                        # 确保没有使用超过存在的数字
                        count = (nums[i] == val) + (nums[j] == val) + (nums[k] == val)
                        if freq[val] > count:
                            ans.add(tuple(sorted([nums[i], nums[j], nums[k], val])))
        
        return [list(x) for x in ans]

count = (nums[i] == val) + (nums[j] == val) + (nums[k] == val)

这行代码是在计算已经选择的三个数中有多少个等于补数 val。这里使用了 Python 中的布尔值到整数的隐式转换:
(nums[i] == val):如果 nums[i] 等于 val,则表达式结果为 True,在数学运算中被当作 1;如果不等于,结果为 False,即 0。
因此,count 的值是这三个条件中为 True 的数量,即在当前已固定的三个数中,有多少个数的值等于 val。如果条件满足,即数组中还有额外的 val 可用,将这个四元组添加到集合 ans 中。

力扣题目链接
题目文章讲解
题目视频讲解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值