代码随想录训练营 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 中。