哈希表/散列表(Hash Table):
哈希函数(Hash Function):
如果hashCode得到的数值 > 哈希表的最大边界 tableSize-1,为了保证映射出来的索引数值都落在哈希表上,会再次对数值做一个取模操作,保证数据一定可以映射到哈希表上
哈希碰撞:
若数据的数量>哈希表的大小,就算哈希函数计算的再均匀,也避免不了会有几个数据同时映射到哈希表上同一个索引的位置(多个key映射到相同索引)
两种解决方法:
常见的三种哈希结构:
总结:
242. 有效的字母异位词
数组就是简单的哈希表,但是数组的大小不能无限开辟(使用数组来做哈希的题目,是因为题目都限制了数值的大小)
只包含小写字母的题目,就很适合用数组(初始化为 [0]*26)
题目描述:若 s
和 t
中每个字符出现的次数都相同,则称 s
和 t
互为字母异位词
分析步骤:
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
# 定义一个数组record,记录字符串s中各字符出现次数
record = [0]*26 # 初始化为0,共26个字母
for i in s:
record[ord(i) - ord('a')] += 1
for i in t:
record[ord(i) - ord('a')] -= 1
for i in range(len(record)):
if record[i] != 0:
return False
return True
383. 赎金信
242. 有效的字母异位词:求 字符串a 和 字符串b 是否可以相互组成
383. 赎金信:求 字符串a 能否组成 字符串b,而不用管 字符串b 能不能组成 字符串a
因为题目只有小写字母,可以用一个长度为26的数组记录magazine里字母出现的次数,然后再用 ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
record = [0]*26 # 只有小写英文字母
if len(ransomNote) > len(magazine):
return False
for i in magazine:
record[ord(i) - ord('a')] += 1
for j in ransomNote:
record[ord(j) - ord('a')] -= 1
if record[ord(j) - ord('a')] < 0:
return False
return True
49. 字母异位词分组
用哈希表来对字符串进行分组,key为字符串的字母序排列,value为相同排列的所有字符串的数组。遍历结束时,将哈希表的value一起返回即可
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
dic = {}
for i in strs:
i_sorted = "".join(sorted(i)) # 对字符串按字母顺序排序
if i_sorted not in dic:
dic[i_sorted] = [i]
else:
dic[i_sorted].append(i)
return list(dic.values())
举个栗子:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
sorted("eat") 的输出是 ['a', 'e', 't'],经过 "".join() 后,输出为 "aet"
这样,把每个单词按字母序排序后的结果作为key,value为排序前的结果
438. 找到字符串中所有字母异位词
思路:哈希(数组)+滑动窗口
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
m, n = len(s), len(p)
result = []
if m < n:
return []
s_cnt = [0]*26
p_cnt = [0]*26
for i in range(n):
p_cnt[ord(p[i]) - ord('a')] += 1
s_cnt[ord(s[i]) - ord('a')] += 1
# 第一个窗口
if p_cnt == s_cnt: # 说明s的前len(p)个字符,就是p的字母异位词
result.append(0) # 返回起始索引0
for i in range(n,m): # i指向字符的最后一位(窗口的右指针)
s_cnt[ord(s[i-n]) - ord('a')] -= 1 # 滑动窗口,剃掉之前窗口的开始位置
s_cnt[ord(s[i]) - ord('a')] += 1 # 新加进来的字母
if s_cnt == p_cnt:
result.append(i-n+1) # 新窗口起始位置
return result
350. 两个数组的交集 II
思路:两个字典分别记录下两个数组里的值(key)和出现次数(value),遍历其中一个字典,若其key在另一个字典中也出现过,返回这个key,且这个key的次数为该key在两个字典中对应的较小value
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
dic1 = {}
dic2 = {}
for i in nums1:
if i in dic1:
dic1[i] += 1
else:
dic1[i] = 1
for j in nums2:
if j in dic2:
dic2[j] += 1
else:
dic2[j] = 1
res = []
for i in dic1:
if i in dic2:
res = res + [i]*min(dic1[i], dic2[i])
return res
349. 两个数组的交集
如果哈希值比较少/特别分散/跨度非常大,使用数组就造成空间的极大浪费。使用数组来做哈希的题目,是因为题目都限制了数值的大小
直接使用set不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的
这题在提示中限制了nums的长度≤1000,故本题可以用数组,也可以用set(区别于242题)来做哈希表
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
result = set() # 自动去重
num_size = set(nums1) # 把nums1转换为哈希表
for i in nums2:
if i in num_size: # 在哈希表里是否出现过
result.add(i)
return list(result)
这题注意,往set里增加元素的方法是add(),以及 set可以自动去重 这两个点即可
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
t1 = [0]*1001
result = set()
# 把nums1处理成哈希表的结构
for i in range(len(nums1)):
t1[nums1[i]] = 1 # 记录nums1里所有出现过的元素
for i in range(len(nums2)):
if t1[nums2[i]] == 1: # 判断哈希表t1里是否出现过nums2的元素
result.add(nums2[i])
return list(result)
总结:
202. 快乐数
求和过程中sum会重复出现,使用哈希法(定义一个集合set)来判断这个sum是否重复出现,若重复则无限循环、始终到不了1(返回False),否则一直找到 sum=1 为止
class Solution:
# 先定义一个算每位平方和的函数
def calculate(self, n: int):
sum = 0
for i in range(len(str(n))):
sum += int(str(n)[i])**2
return sum
def isHappy(self, n: int) -> bool:
record = set() # 定义一个集合,判断sum是否出现过
res = self.calculate(n)
while res != 1:
if res in record: # 若出现过,说明无限循环但始终变不到1
return False
record.add(res) # 将结果添加进集合中
res = self.calculate(res)
return True
1. 两数之和
使用数组和set的局限:
- 数组的长度是受限制的,如果元素很少而哈希值很大,则会造成内存空间的浪费
- set是一个集合,里面放的元素只能是一个key
而本题不仅要判断y是否存在,还要记录y的下标位置,因为要返回x和y的下标,因此要用另一种数据结构 — map,它是一种
的存储结构,可以用key保存数值,value保存数值所在的下标
思路:需判断一个元素是否遍历过。把遍历过的元素加到一个集合里(map存放遍历过的元素),每次遍历一个新的位置之后,判断想要寻找的这个元素是否在这个集合里出现过(元素为key,下标为value,map结构即查找key是否在map里出现过)
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
# 定义一个字典存放遍历过的元素
dic = {}
for i in range(len(nums)):
s = target - nums[i] # 要去字典里查询的值为s
if s in dic:
return [dic.get(s), i]
dic[nums[i]] = i # key是元素,value是下标
return []
- 查一个字典中有没有为某值的key,直接用关键字in即可
- 字典中增加一对键值对,直接 dict[key] = value 即可
454. 四数相加 II
思路:
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
dic = {} # key为遍历nums1和nums2数组得到的两数之和,value为和的出现次数
for a in nums1:
for b in nums2:
if a+b not in dic:
dic[a+b] = 1
else:
dic[a+b] += 1
count = 0
for c in nums3:
for d in nums4:
target = 0-(c+d) # 因为a+b+c+d=0
if target in dic:
count += dic[target]
return count
15. 三数之和
思路:双指针法,而非哈希法(因为要剔除重复的三元组)
重点:如何去重?
if nums[i] == nums[i-1] and i>0:
continue
while right > left and nums[left] == nums[left+1]:
left += 1
while right > left and nums[right] == nums[right-1]:
right -= 1
完整代码:
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
return result
if nums[i] == nums[i-1] and i>0:
continue
left, right = i+1, len(nums)-1
while left < right: # 不能=,若left=right,则为同一个数,不是三数之和了
sums = nums[i] + nums[left] + nums[right]
if sums > 0:
right -= 1
elif sums < 0:
left += 1
else:
result.append([nums[i], nums[left], nums[right]])
# 以下为后面两个数的去重逻辑,应放在找到一个三元组之后
while right > left and nums[left] == nums[left+1]:
left += 1
while right > left and nums[right] == nums[right-1]:
right -= 1
right -= 1
left += 1
return result
18. 四数之和
注意:target 不一定 =0
思路:双指针法,在三数之和外面再套一层for循环,顺序:k、i(k后一位)、left(i后一位)、right(数组最后一位)
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
result = []
nums.sort() # 先将数组排序
for k in range(len(nums)):
if nums[k] > target and nums[k] > 0 and target > 0: # 一级剪枝
break
if k > 0 and nums[k] == nums[k-1]: # 一级去重
continue
for i in range(k+1, len(nums)):
if nums[k]+nums[i] > target and nums[k]+nums[i] > 0 and target > 0: # 二级剪枝
break
if i > k+1 and nums[i] == nums[i-1]: # 二级去重
continue
left, right = i+1, len(nums)-1
while left < right:
sums = nums[k] + nums[i] + nums[left] + nums[right]
if sums < target:
left += 1
elif sums > target:
right -= 1
else:
result.append([nums[k], nums[i], 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
return result