代码随想录第六天哈希表part01|454.四数相加II、383. 赎金信、15.三数之和、18.四数之和
454.四数相加II
思路
:
与四数之和的区别:四数之和要考虑去重
①暴力法求解:4层for循环,时间复杂度O(n^4)
②利用哈希表求解:假设数组列表为:A,B,C,D
遍历A,B数组,将数组相加后的值放到一个集合里,遍历C,D数组,判断集合里有没有我们想要的元素。
数据结构的选择:因为数组中存储的值可能会很大,考虑set或map;因为要统计A+B出现过的次数,选择map数据结构。
注意: 在判断map中存在我们需要的值之后,不是count=count+1,而是count=count+map[key]
伪代码(C++):
unordered_map(int, int)
for(a:A)
for(b:B)
map[a+b]++;
for(c:C)
for(d:D)
target = 0 - (c+d)
if(map.find(target != map.and)) // 在map中找到了target
count += map[target]
python代码
:
补充:python中字典的一些方法:
①dict.get(key[, value])
key – 字典中要查找的键。
value – 可选,如果指定键的值不存在时,返回该默认值。
②key in dict:判断字典dict中是否存在key这个键
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
hashmap = {}
count = 0
for a in nums1:
for b in nums2:
hashmap[a+b] = hashmap.get(a+b, 0) + 1
for c in nums3:
for d in nums4:
target = 0 - c - d
count += hashmap.get(target, 0)
return count
// 版本二:还没细看
from collections import defaultdict
class Solution:
def fourSumCount(self, nums1: list, nums2: list, nums3: list, nums4: list) -> int:
rec, cnt = defaultdict(lambda : 0), 0
for i in nums1:
for j in nums2:
rec[i+j] += 1
for i in nums3:
for j in nums4:
cnt += rec.get(-(i+j), 0)
return cnt
383. 赎金信
思路
:
该题较为简单,考虑用哈希表来做,需要记录元素出现的次数,可以使用数组或者map,由于这里固定randomNote和magazine都只由小写英文字母组成,可以选择数组这一数据结构。
一些同学可能想,用数组干啥,都用map完事了,其实在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!
python代码
:
补充:
①python中字典的get()方法dict.get(key[, value])
key – 字典中要查找的键。
value – 可选,如果指定键的值不存在时,返回该默认值。
②用ord(‘a’)获得字符a的ASCII码
// 使用数组
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
hashlist = [0]*26
for b in magazine:
hashlist[ord(b) - ord('a')] = hashlist[ord(b) - ord('a')] + 1
for a in ransomNote:
if hashlist[ord(a) - ord('a')]:
hashlist[ord(a) - ord('a')] -= 1
else:
return False
return True
# 使用字典 map
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
hashmap = dict()
for b in magazine:
hashmap[b] = hashmap.get(b, 0) + 1
for a in ransomNote:
if hashmap.get(a, 0):
hashmap[a] = hashmap[a] - 1
else:
return False
return True
15. 三数之和
思路:
两个for循环确定a+b,判断a,b,c是否同时满足时,只需判断a+b是否在集合中存在。该题的关键也是在于去重,不太建议使用哈希法做该题,因为去重的细节太多了,很难做到bug free。可以使用双指针法
双指针法:
思路:
对数组进行排序,该题返回的是元素值而不是下标所以可以排序。
指针i得到a,剩下的位置,left,right
如果 nums[i] + nums[left] + nums[right] > 0,让right前移一位,right–
如果 nums[i] + nums[left] + nums[right] < 0,让left后移一位,left++
如果 nums[i] + nums[left] + nums[right] = 0,结果:nums[i], nums[left], nums[right
去重:结果集中不能有重复的三元组,
伪代码 (C++)
注意: ①nums[i]的去重 注意是i-1不是i+1;②nums[left]和nums[right]的去重,注意是while不是if,并且nums[right] = nums[right-1]而不是 nums[right] = nums[right+1],同理right
sort(nums)
result = [ ]
for(i=0; i<nums.size; i++)
if(nums[i]>0) // 第一个数大于0,无解
return
if(i>0 && nums[i] == nums[i-1]) // nums[i]的去重 注意是i-1不是i+1
continue
left = i+1
right = num.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
result.push([nums[i], nums[left], nums[right]])
// left和right的去重
while(left<right && nums[right] == nums[right-1]) right--
while(left<right && nums[left] == nums[left+1]) left++
right-- left ++
python代码
python中divmod函数介绍:divmod()函数把除数和余数运算结果结合起来
例如,divmod(a, b),返回一个包含商和余数的元组(a // b, a % b)
class Solution:
def threeSum(self, nums):
nums.sort()
n = len(nums)
result = []
for i in range(n-1):
if nums[i]>0:
return result
if i>0 and nums[i] == nums[i-1]:
continue
l, r = i+1, n-1
while l<r:
if nums[i]+nums[l]+nums[r] >0 :
r-=1
elif nums[i]+nums[l]+nums[r] < 0:
l+=1
else:
result.append([nums[i], nums[l], nums[r]])
while l<r and nums[l] == nums[l+1]:
l+=1
while l<r and nums[r] == nums[r-1]:
r-=1
l+=1
r-=1
return result
# 以前写的代码:关键在于去重
class Solution:
def threeSum(self, nums):
n = len(nums)
result = []
nums.sort()
for i in range(n-2):
if nums[i] + nums[i+1] + nums[i+2] > 0:
break
if nums[i] + nums[n-2] + nums[n-1] < 0:
continue
if i > 0 and nums[i] == nums[i-1]: # 对于[-4,-1,-1,0,1,2] 去重,当起始的值等于前一个元素,那么得到的结果将会和前一次相同 [-1,0,1] [-1,0,1]
continue
l, r = i+1, n-1
while l < r:
tmp = nums[i] + nums[l] + nums[r]
if tmp == 0:
result.append([nums[i], nums[l], nums[r]])
while l+1 < r and nums[l] == nums[l+1]:
l += 1
l+=1
while l<r-1 and nums[r] == nums[r-1]:
r -= 1
r-=1
elif tmp < 0:
l += 1
else:
r -= 1
return result
18. 四数之和
思路:
延续三数之和的思路,再套用一个for循环。
主要在于剪枝和去重的操作,剪枝误区: 误以为排序后nums[0]>target不符合题意,若nums[0]和target都为负数,并且nums[1]也<0,则会出现两数相加后更小。正确剪枝:nums[k]>0&&target>0
python代码
注意while中的边界条件需要仔细考虑,while curr而不是while curr.next,因为最后一个是空指针,temp = curr.next
# 版本一 双指针
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: List[int], target: int) -> List[List[int]]:
result = []
nums.sort()
n=len(nums)
for i in range(n-3):
if nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target:
break
if nums[i]+nums[n-1]+nums[n-2]+nums[n-3]<target:
continue
if i>0 and nums[i] == nums[i-1]: #去重,当起始的值等于前一个元素,那么得到的结果将会和前一次相同
continue
for j in range(i+1,n-2):
if j>i+1 and nums[j] == nums[j-1]: #去重,当起始的值等于前一个元素,那么得到的结果将会和前一次相同
continue
l=j+1
r=n-1
while(l<r):
total = nums[i]+nums[j]+nums[l]+nums[r]
if total==target:
result.append([nums[i],nums[j],nums[l],nums[r]])
while l+1<r and nums[l]==nums[l+1]:
l+=1
l+=1
while r-1>l and nums[r]==nums[r-1]:
r-=1
r-=1
elif total<target:
l+=1
else:
r-=1
return(result)
# 版本二 使用字典
class Solution(object):
def fourSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[List[int]]
"""
# 创建一个字典来存储输入列表中每个数字的频率
freq = {}
for num in nums:
freq[num] = freq.get(num, 0) + 1
# 创建一个集合来存储最终答案,并遍历4个数字的所有唯一组合
ans = set()
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]
时间复杂度:O(n^3)
空间复杂度:O(n)
总结
一般来说哈希表都是用来快速判断一个元素是否出现集合里。
对于哈希表,要知道哈希函数和哈希碰撞在哈希表中的作用。
哈希函数是把传入的key映射到符号表的索引上。
哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法和线性探测法。
接下来是常见的三种哈希结构:
数组
set(集合)
map(映射)
在C++语言中,set 和 map 都分别提供了三种数据结构,每种数据结构的底层实现和用途都有所不同。
数组作为哈希表
在242.有效的字母异位词中,我们提到了数组就是简单的哈希表,但是数组的大小是受限的!
这道题目包含小写字母,那么使用数组来做哈希最合适不过。
在383.赎金信 (opens new window)中同样要求只有小写字母,那么就给我们浓浓的暗示,用数组!
本题和242.有效的字母异位词很像,242.有效的字母异位词是求字符串a 和字符串b 是否可以相互组成,在383.赎金信中是求字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a。
一些同学可能想,用数组干啥,都用map不就完事了。
上面两道题目用map确实可以,但使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以数组更加简单直接有效!
set作为哈希表
在349. 两个数组的交集 (opens new window)中我们给出了什么时候用数组就不行了,需要用set。
这道题目没有限制数值的大小,就无法使用数组来做哈希表了。
主要因为如下两点:
数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
所以此时一样的做映射的话,就可以使用set了。
在202.快乐数中,我们再次使用了unordered_set来判断一个数是否重复出现过。
map作为哈希表
在1.两数之和 (opens new window)中map正式登场。
来说一说:使用数组和set来做哈希法的局限。
数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
map是一种<key, value>的结构,本题可以用key保存数值,用value在保存数值所在的下标。所以使用map最为合适。
C++提供如下三种map:(详情请看关于哈希表,你该了解这些!
std::map
std::multimap
std::unordered_map
std::unordered_map 底层实现为哈希,std::map 和std::multimap 的底层实现是红黑树。
同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解),1.两数之和 (opens new window)中并不需要key有序,选择std::unordered_map 效率更高!
在454.四数相中我们提到了其实需要哈希的地方都能找到map的身影。
本题咋眼一看好像和18. 四数之和,15.三数之和差不多,其实差很多!
关键差别是本题为四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑重复问题,而18. 四数之和 (opens new window),15.三数之和 (opens new window)是一个数组(集合)里找到和为0的组合,可就难很多了!
用哈希法解决了两数之和,很多同学会感觉用哈希法也可以解决三数之和,四数之和。
其实是可以解决,但是非常麻烦,需要去重导致代码效率很低。
在15.三数之和 (opens new window)中我给出了哈希法和双指针两个解法,大家就可以体会到,使用哈希法还是比较麻烦的。
所以18. 四数之和,15.三数之和都推荐使用双指针法!