哈希表
目录
哈希表基础知识
- 哈希表/散列表 (hashmap):是根据关键码的值而直接进行访问的数据结构。
数组就是一张哈希表,数组索引即为关键码 - 应用范围:一般哈希表都是用来快速判断一个元素是否出现集合里,查找的时间复杂度为O(1)
- python中的常见哈希结构:
数组[]
、集合hashset = set()——相当于仅有key的字典
、字典hashmap = {}
各自应用范围:
数组:当数据量小于1000,且数据之间较连续 如:1 2 4 5 8;注意:若数值间间隔很大则不适合使用数组,会浪费很多内存,如:1 2 1000 存三个量却占用1000个存储单位(由于数组存储空间连续)。通过下标查找元素时间复杂度O(1),若是查找一个元素是否在数组中出现,时间复杂度O(n)
集合:数据量不宜过大,数据不重复,无序,查找某个元素方式为哈希,时间复杂度O(1)
设置一个空集合: set()
设置有数据的集合:{‘haha’,‘heihei’,‘neinei’}
注:add() 为集合添加元素; pop() 随机移除元素
字典:同时需要存储key和val
设置一个空字典:dict{}
设置有数据的字典:{‘haha’: 1,‘heihei’: 2,‘neinei’: 3}
- 若使用字符作为关键值,需要使用哈希函数 (hash function)
hash code 是使用特定编码方式,将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。
如果学生的数量大于哈希表的大小,此时会出现有几位学生的名字同时映射到哈希表同一个索引下标的位置——哈希碰撞。一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
242 有效的字母异位词
题目
分析
若s和t中字符同且字符数也相同,则s和t是字母异位词。统计s中出现的字符并统计出现的次数,以字符做key,字符数做val,构建哈希表。遍历t中的字符,出现一次则对哈希表中的值减一。若是有效的字母异位,最终的hashmap应该全0
由于字符全为小写字符,字符数26,使用ASCII的相对值,可使用数组构造哈希表。
代码
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
hashmap = [0]*26
for i in s:
hashmap[ord(i)-ord('a')] += 1
for i in t:
hashmap[ord(i)-ord('a')] -= 1
for i in hashmap:
if i != 0:
return False
return True
注:ord() :返回对应字符的ascii码
进阶——不可直接使用数组了,可用字典构建哈希表
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if len(s)!=len(t):
return False
hashmap = {}
for i in s:
if i in hashmap:
hashmap[i] += 1
# 不用ord()则索引是字符 内部使用了hash函数 即通过hashcode将其他字符转为数字
else:
hashmap[i] = 1
for i in t:
if i in hashmap:
hashmap[i] -= 1
if hashmap[i] < 0: # 字符一样 数量有多有少,则一定有负
return False
else:
return False # 出现s中没有的字符 直接False
return True
349 两个数组的交集
题目
分析
- 构建字典型的哈希表,nums1中出现的数作为key,val设为1;
- 遍历nums2中的元素
① 若该元素在哈希表中且val不等于0(说明该数第一次出现),则将该元素加到返回表中,val设为0;
② 若该元素在哈希表但val等于0(说明该数已经出现过了),无需操作;
③ 若该元素不在哈希表中,也无需操作。最后返回返回表即可
通过对val的更改实现结果的去重
代码
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
res =[]
hashmap = {}
for i in nums1:
if i not in hashmap: # hashmap中保存nums1中出现的所有元素 val=1
hashmap[i] = 1
for i in nums2:
if i in hashmap and hashmap[i] == 1:
res.append(i) # nums1和nums2中相同的元素加到res中 赋为0是为了结果中元素唯一
hashmap[i] = 0
return res
202 快乐数
题目
分析
由于题目给出不快乐数会进入循环,即在替换过程中,会有数字重复出现。我们可以将已出现的数字保存在hashset中,若某个数字重复出现,则立即返回False,否则当出现’1’时,即可认为该数为快乐数,返回True
代码
class Solution:
def isHappy(self, n: int) -> bool:
hashset = set()
while n not in hashset:
if n == 1:
return True
hashset.add(n) # set: .add() .remove()
sqrsum = 0
while n != 0: # 按位取平方 求和
sqrsum = sqrsum + (n%10)**2
n = n//10
n = sqrsum
return False
注意:对整数按位取平方求和的方法 除以10取余
集合:
- 创建
大括号 { } / set() 函数
注意:创建一个空集合必须用 set() 而不是 { },因为 { } 是用来创建一个空字典。 - 运算
交集 & : x&y,返回一个新的集合,包括同时在集合 x 和y中的共同元素。
并集 | : x|y,返回一个新的集合,包括集合 x 和 y 中所有元素。
差集 - : x-y,返回一个新的集合,包括在集合 x 中但不在集合 y 中的元素。
补集:通过差集可实现补集 - 操作
添加元素
.add() :添加的元素,只能是数字、字符串、元组或者布尔类型(True 和 False)值(不可变),不能添加列表、字典、集合这类可变的数据。如果元素已存在,则不进行任何操作(集合的互异性)。
.update():传入的参数必须是可迭代对象(列表,元组,字典等)(可用于添加一个元素、或者多个元素)。若set.updata(‘python’),则会被按元素拆分;若set.updata([python]),则输入参数被看作集合,添加的是python。由于’python’使字符串,不可修改
删除元素
.remove() :如果被删除元素本就不包含在集合中,则此方法会抛出 KeyError 错误
.discard() :和remove同,唯一区别在于,当删除集合中元素失败时,此方法不会抛出任何错误。
.pop():随机删除
清空集合:.clear()
计算集合元素个数:len()
a = {"Hello", "HaiCoder", 1024}
a.update("Python")
print("Set =", a)
b = {"Hello", "HaiCoder", 1024}
b.update(['Python', 'matlab']) # 放进集合
print("Set =", b)
结果:
Set = {1024, 'y', 't', 'n', 'Hello', 'P', 'HaiCoder', 'o', 'h'}
Set = {1024, 'matlab', 'Hello', 'Python', 'HaiCoder'}
1 两数之和
题目
代码
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
n = len(nums)
hashmap = {}
for i in range(n): # i是下标
diff = target - nums[i]
if diff in hashmap:
return [hashmap[diff],i]
else:
hashmap[nums[i]] = i # 数值作为键,下标作为值
注:要求返回下标,不可使用双指针
454 四数相加Ⅱ
题目
分析
只要求输出满足的个数 不要求下标
# 一种方法是四层循环 时O(n^4)
# 另一种是先nums1和num2组合,求出所有可能的和;在nums3和nums4组合,求出所有可能和;就退化为两数和问题,时O(n^2) 空O(n^2)——分组+哈希表
注:3 4 无需使用哈希表存 只要验证3 4 组合的相反数在1 2 的哈希表中是否出现即可 节省内存
代码
res = 0
hashmap = {}
# 1 2分为一组
for i in range(len(nums1)):
for j in range(len(nums2)):
a = nums1[i] + nums2[j]
if a in hashmap:
hashmap[a] += 1
else:
hashmap[a] = 1
# 3 4分为一组
for i in range(len(nums3)):
for j in range(len(nums4)):
diff = -nums3[i]-nums4[j]
if diff in hashmap:
res = res + hashmap[diff] # hashmap中数值为key, 出现的次数为val
return res
383 赎金信
题目
分析
若magazine 里的字符数大于等于ransonNote中字符 则为True
- 构建字典型哈希表,统计magazine中字符(key)和数量(val)
- 遍历ransoNote中元素,
若元素在哈希表中,每出现一次则减1
若元素不在哈希表中或在哈希表但val<0的,则false
# 注:小写英文字符 可用ord(i)-ord(‘a’) 存在长26的数组
代码
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
# 小写英文字符 可用ord(i)-ord('a') 存在长26的数组 同242.有效的字母异位词
# 以相对差做索引 出现次数做val magazine中出现一次val+1 ransomNote中出现一次val-1
if len(magazine)<len(ransomNote):
return False
res = [0]*26
for i in magazine:
idx = ord(i)-ord('a')
res[idx] += 1
for i in ransomNote:
idx = ord(i)-ord('a')
res[idx] -= 1
if res[idx] < 0 :
return False
return True
15 三数之和
题目
分析
只要求三元数组,不要求下标,可先转为有序数组,再先固定一个,另两个就是两数和问题 ——双指针
对同一指针的相同元素跳过,可实现去重
代码
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
# 所有和为 0 且不重复的三元组
# 有序可以实现去重
n = len(nums)
res = []
nums.sort()
for i in range(n-2):
if i>0 and nums[i]==nums[i-1]:
continue # 对i去重
j = i+1
k = n-1
while j<k:
if nums[i]+nums[j]+nums[k]<0: # j右移
while j<k-1 and nums[j]==nums[j+1]:
j +=1
j+=1
elif nums[i]+nums[j]+nums[k]>0:
while j<k-1 and nums[k]==nums[k-1]:
k -=1
k-=1
else:
res.append([nums[i],nums[j],nums[k]])
while j<k-1 and nums[j]==nums[j+1]:
j +=1
while j<k-1 and nums[k]==nums[k-1]:
k -=1
j+=1
k-=1
return res
18 四数之和
题目
分析
同15 三数和问题 这里固定两个指针
代码
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
res = []
n = len(nums)
if len(nums)<4: # 剪枝
return res
nums.sort()
for a in range(n-3):
if a>0 and nums[a]==nums[a-1]: # 剪枝 去重
continue
if sum(nums[a:a+4])>target: # 剪枝
return res
for b in range(a+1,n-2):
if b>a+1 and nums[b]==nums[b-1]:
continue
c = b + 1
d = n - 1
while c<d:
if nums[a]+nums[b]+nums[c]+nums[d]<target:
c+= 1
elif nums[a]+nums[b]+nums[c]+nums[d]>target:
d-=1
else:
res.append([nums[a],nums[b],nums[c],nums[d]])
while c<d-1 and nums[c]==nums[c+1]:
c+= 1
while c<d-1 and nums[d]==nums[d-1]:
d-= 1
c+=1
d-=1
return res
注:# 数组切片nums[0:4] range(i+1,n-2) 一个冒号一个逗号 都是左闭右开
560 和为 K 的子数组
思路:统计前缀和,若两前缀和presum[i]和presum[j]的差为k,说明i+1到j这一段和为k
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
# 前缀和 记录nums[0]到nums[i]的和
mp = {}
mp[0] = 1
counter = 0
presum = 0
for i in range(len(nums)):
presum += nums[i] # nums[0]到nums[i]的和
diff = presum - k # 若想当前前缀和减i之前的前缀和为k,则i之前的前缀和应等于diff
if diff in mp: # 若diff在mp中,说明nums[i]之前有前缀和为diff的
counter += mp[diff] # mp[diff]为在当前presun下,其左边序列中前缀和等于diff的序列的数目
if presum in mp: # 别忘了将当前节点的前缀和也加入mp中
mp[presum] += 1
else:
mp[presum] = 1
return counter
时,空O(N)
1647. 字符频次唯一的最小删除次数
class Solution:
def minDeletions(self, s: str) -> int:
# 用哈希表统计字符出现的次数 key为字符,val为次数
# 再遍历一遍哈希表,将次数存入set中,若出现一个字符的次数已在set中,就将其次数减1再比较,直至不与set中元素重复且>=0,每次减1操作次数都加1
hp1 = {}
for i in s:
if i in hp1:
hp1[i] += 1
else:
hp1[i] = 1
nums = set()
freq = 0
for val in hp1.values(): # 对所有出现过的字符遍历
while val in nums:
val -= 1
freq += 1
if val > 0:
nums.add(val)
return freq
128. 最长连续序列
思路:包含nums[i]的最长序列长度因为包含其左节点的最长序列长+包含其右结点的最长序列长度
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
hashmap = {}
ans = 0
for i in range(len(nums)):
if nums[i] not in hashmap:
left = hashmap.get(nums[i]-1, 0) # 获取左右相邻字符串的长度
right =hashmap.get(nums[i]+1, 0)
length = left+right+1 # 包括nums[i]的序列的长度
ans = max(length, ans) # 更新最长序列长度
hashmap[nums[i]] = length
hashmap[nums[i]-left] = length # 更新左右边界的序列长度 中间不用更新 之后不会使用
hashmap[nums[i]+right] = length
return ans