算法基础——哈希表(散列表)
1、预备
一种牺牲空间换时间的结构,在需要查询一个元素是否出现过或出现次数时可以考虑用hash。
- 拉链法(开散列法)
- 开地址法(闭散列法)
解决冲突
:线性探查法、二次探查法、双散列法。
删除时
,散列表使用时间过久,empty标记大多数为false,搜索时间效率降低,所以散列表经过一段时间的使用后需要重新组织,如重新设置empty值。
在python中三种可模拟hash的结构数组、字典、集合使用场景不同。
在Python中,
列表(list)
使用数组(array)来实现其底层数据结构。数组是一种连续存储相同类型元素的数据结构,通过索引来访问和操作其中的元素。由于数组的长度固定,当列表需要进行动态扩展时,Python会创建一个新的更大的数组,并将原数组中的元素复制到新的数组中。
集合(set)
在Python中使用哈希表(hash table)来实现其底层数据结构。哈希表是一种通过将元素的键转换为数组索引来快速查找和插入元素的数据结构。在哈希表中,每个元素的键都会通过哈希函数转换为一个唯一的索引值,通过该索引值可以直接访问元素。
字典(dictionary)
也使用哈希表作为其底层数据结构。字典中的每个元素由键值对(key-value pair)组成,其中键用于进行查找和插入操作,值则表示与该键相关联的数据。字典通过将键转换为索引值来快速访问和操作数据。
需要注意的是,列表、集合和字典在Python中都是可变的数据结构,可以动态地添加、删除和修改其中的元素。
2、应用
2.1 有效的字母异位词 leetcode242
- 暴力对比
如果先用list()分割为列表,再for循环比对的话,复杂度O(m*n)。
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
l1=list(s)
for i in t:
if not(i in l1):
return False
else:
l1.remove(i)
return True if l1==[] else False
- 数组记录字幕出现次数
实质上,是避免了s1每一个字母都和s2每一个字母比对。我们只需要各自循环一次,记录字母出现次数是否相同即可。
ord()
:获取ascii值
chr()
:将ascii值转为符号
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
l=[0]*26
for i in s:
l[ord(i)-ord('a')]+=1
for j in t:
l[ord(j)-ord('a')]-=1
for m in l:
if m!=0:
return False
return True
2.2 两个数组的交集 leetcode 349
- hash表
这里其实没有用集合实现,而是用字典实现,尽管py中集合底层使用hash实现的,in判断时间都是常数,但字典能避免重复加入相同交集元素,同时集合比字典实现慢一点。
class Solution: # 字典实现
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
hash={}
res=[]
for i in nums1:
if not hash.get(i):
hash[i]=1
for j in nums2:
if hash.get(j):
hash[j]=0
res.append(j)
return res
class Solution: # 集合实现
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
hash=set() # set边界符号{}和字典一样,但直接hash={}会成为字典类型不能用add操作
res=set()
for i in nums1:
hash.add(i)
for j in nums2:
if j in hash:
res.add(j)
return list(res)
ps
:什么时候用数组,什么时候用字典和集合呢?如果是像2.1那样固定长度的,且填充率较高那数组的随机存取会快不少。用set占用空间比数组快,但速度比数组慢,做hash计算的耗时,在数据量大的情况,比较庞大。
- 双指针(对于排序好的nums1,nums2)
略
2.3 快乐数 leetcode202
- hash
本题关键在于理解题目中所说的出现循环,从而知道是一类hash题。
class Solution:
def isHappy(self, n: int) -> bool:
def sumsquare(num):
ans=0
while num>0:
ans+=(num%10)**2
num=num//10
return ans
ans=n
s=set()
while ans!=1 and ans not in s:
s.add(ans)
ans=sumsquare(ans)
return ans==1# 在return中直接判断
时间复杂度
:本题时间复杂度较难分析,首先需要理解快乐数。
对于第三种情况的时间复杂度需要数学推导,这里仅摘录结果。
空间复杂度:
与时间复杂度密切相关的是衡量我们放入哈希集合中的数字以及它们有多大的指标。对于足够大的 n,大部分空间将由n本身占用。我们可以很容易地优化到 O(243⋅3)=O(1),方法是只保存集合中小于243的数字,因为对于较高的数字,无论如何都不可能返回到它们。
- 双指针(快慢)
当然,考虑到出现循环的现象也可以考虑双指针
def isHappy(self, n: int) -> bool:
def get_next(number):
total_sum = 0
while number > 0:
number, digit = divmod(number, 10)
total_sum += digit ** 2
return total_sum
slow_runner = n
fast_runner = get_next(n)
while fast_runner != 1 and slow_runner != fast_runner:
slow_runner = get_next(slow_runner)
fast_runner = get_next(get_next(fast_runner))
return fast_runner == 1
2.4 两数之和 leetcode1
- hash
考虑前面有没有符合条件配对的数,这样只需要遍历一遍。
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
s={}
for i in range(len(nums)):
j=target-nums[i]
if j not in s:
s[nums[i]]=i
else:
return [i,s[j]]
2.5 四数相加Ⅱ leetcode
最开始写了个效率很低(大概刚过leetcode)的循环,后面发现解法就这样,主要想通过把第一二个数组和三四个分开得到一个字典,再遍历字典,总体复杂度为O(n^2)
哈希的题有些就这样看上去是暴力,我开始也没想到两个一组分开,其实就是分治降了时间复杂度,毕竟答案只是计数。
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
def newdict(l1: List[int],l2: List[int])->dict:
d={}
for i in range(len(l1)):
for j in range(len(l2)):
n=l1[i]+l2[j]
if n not in d:
d[n]=1
else:
d[n]+=1
return d
ans=0
d1=newdict(nums1,nums2)
d2=newdict(nums3,nums4)
for key1,value1 in d1.items():
for key2,value2 in d2.items():
if key1+key2==0:
ans+=value1*value2
return ans
实际上写得太复杂了可以简化。用get方法来获取该键对应的值,如果该键不存在则默认值为0。然后将该键对应的值加1。
dict.items() 返回由字典键值对组成的迭代对象。
而enumerate只能用于枚举对象,返回下标索引和值,注意两者区别
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
record = dict()
count = 0
for x in nums1:
for y in nums2:
record[x+y] = record.get(x+y, 0) + 1
for x in nums3:
for y in nums4:
if -x-y in record:
count += record[-x-y]
return count
2.6 赎金信 leetcode 383
比较简单
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
d={}
for i in magazine:
d[i]=d.get(i,0)+1
for j in ransomNote:
if d.get(j,0)==0:
return False
else:
d[j]-=1
return True
2.7 三数之和 leetcode 15
本题用排序+双指针会好很多,而用哈希存,不容易消除重复的。
- 排序+双指针
外层第一个循环遍历头指针指向,每次循环将i,j设置在相应位置。重点在各种情况的判断和循环不变式的保持。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res,k=[],0
for k in range(len(nums)-2):
if nums[k]>0:break # k对应最小值需要小于零
if k>0 and nums[k]==nums[k-1]:continue #重复跳过
i,j=k+1,len(nums)-1
while i<j:
s=nums[k]+nums[i]+nums[j]
if s<0:
i+=1
while i<j and nums[i]==nums[i-1]:i+=1
elif s>0:
j-=1
while i<j and nums[j]==nums[j+1]:j-=1
else:
res.append([nums[k],nums[i],nums[j]])
i+=1
j-=1
while i<j and nums[i]==nums[i-1]:i+=1
while i<j and nums[j]==nums[j+1]:j-=1
return res
2.8 四数之和 leetcode 18
同三数之和思路,做一些判断上的调整。
这两道题中,排序杜绝了[1,2,3,4]-[1,2,4,3]这种情况的发生,右侧指针指向的值都大于等于左侧。每个最外层循环,遍历第一个指针为nums[i]时所有组合,if i>0 and nums[i]==nums[i-1]:continue
与内层的+=-=一样都是因为已经考虑了这个数的可能,就离开这个数。
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
nums.sort()
res=[]
for i in range(len(nums)-3):
if target<=0 and nums[i]>0:return res
if i>0 and nums[i]==nums[i-1]:continue
for j in range(i+1,len(nums)-2):
if j>i+1 and nums[j]==nums[j-1]:continue #这一行极容易忽略
m,n=j+1,len(nums)-1
while m<n:
ans=nums[i]+nums[j]+nums[m]+nums[n]
if ans>target:
n-=1
while m<n and nums[n]==nums[n+1]:n-=1
if ans<target:
m+=1
while m<n and nums[m]==nums[m-1]:m+=1
if ans==target:
res.append([nums[i],nums[j],nums[m],nums[n]])
n-=1
m+=1
while m<n and nums[n]==nums[n+1]:n-=1
while m<n and nums[m]==nums[m-1]:m+=1
return res
当然也有人用字典+遍历+集合去重来做。适用于python但技巧性稍微差一点
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]
3、总结
在c++中,由于可以用数组、set、map来做哈希表,故要考虑各自特性。
而python中,主要使用的就是数组和字典。
对于效率而言,一定要熟悉自己使用语言对这些结构的底层实现,才能更好的运用。
数组
数组是定长的,并且键是隐藏的下标索引,故适用于已经知道长度和对下标映射的,如242.有效的字母异位词和383.赎金信。
字典
而对于349. 两个数组的交集,1.两数之和,202.快乐数这种没有限制大小的、数值跨度太大,就不能用数组了(除非你自己设计映射规则和冲突规则,构建hash)
而18. 四数之和、15.三数之和用hash是很难解决的,且每层循环能做的剪枝很有限,此时多指针法体现出优势