学习计划:3个月内算法小成,完成leetcode hot100
当前进度:学完数组、链表
刷题语言:Python
时间:2024/12/07-2024/12/17
三、哈希表
学习链接:代码随想录
1.哈希表理论基础
哈希表(Hash table,散列表)
哈希表的关键就是使用索引直接访问元素,类似使用数组下标访问数组中的元素。
哈希表解决的问题:用于快速判断一个元素是否出现在集合内。类似查询一个名字是都在这所学校的名单上,如果使用枚举的话,时间复杂度是O(n),但是使用哈希表时间复杂度是O(1)。
哈希函数
哈希函数:把学生的姓名映射到哈希表上的索引,然后就可以通过查询索引快速知道这位同学是否在这个学校内了。
通过hashCode把名字转化成哈希值(一种编码方式),这样就可以把字符串映射到哈希表上的索引数字了。
如果哈希编码后得到的数值大于哈希表的大小tableSize,为了保证映射出来的索引数值都落在哈希表上,需要对哈希值做一个取模操作,这样就可以保证学生的姓名一定落在哈希表上了,但是可能会出现好几位学生的名字映射到同一个哈希值上,这就是哈希碰撞。
哈希碰撞
解决方法:拉链法,线性探测法
拉链法:将发生冲突的元素都保存在链表中,先通过哈希值定位到链表,再在链表中找。但是需要选择合适的哈希表大小,这样才不会应为数组空值而浪费大量内存空间,也不会因为链表太长而在查找上浪费大量时间。
线性探测法:使用线性探测法一定要保证tablesize大于datasize,我们需要依靠哈希表中的空位来解决碰撞问题。例如小李已经放在哈希表中1的位置,此时小张的哈希值也是1,那么就向下找一个空位放置小王的信息。
常见的三种哈希结构
- 数组
- set(集合)
- map(映射)
2.有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1: 输入: s = "anagram", t = "nagaram" 输出: true
示例 2: 输入: s = "rat", t = "car" 输出: false
说明: 你可以假设字符串只包含小写字母。
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
#使用数组作为哈希表
res = [0]*26 #字母一共有26个,用一个长度26的列表保存每个字母的个数
#对s进行遍历,对应的字母+1
for i in range(len(s)):
res[ord(s[i])-ord('a')] +=1 #ord()用于获取字母的ascii码值,a-z是按顺序递增的
#对t进行遍历,对应的字母-1
for i in range(len(t)):
res[ord(t[i])-ord('a')] -=1
#如果s和t的字母完全对应,则最后的res都是0
for i in range(26):
if res[i]!=0:
return False
return True
3.两个数组的交集
题意:给定两个数组,编写一个函数来计算它们的交集。
#改用set的时候还是得用set
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
#set() 函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,还可以计算交集、差集、并集等
#&计算交集,|计算并集,-计算差集
return list(set(nums1) & set(nums2))
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
#使用字典作为哈希表
table={}
#nums1 = [1,2,2,1], nums2 = [2,2]
for num in nums1:
table[num] = table.get(num,0)+1
#使用集合保存结果
res = set()
for num in nums2:
if num in table:
res.add(num)
del table[num]
return list(res)
4.快乐树
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。
例如19:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1
class Solution:
def isHappy(self, n: int) -> bool:
res = [] # res=set()
while True:
n = self.get_sum(n)
if n==1:
return True
if n in res:
return False
else:
res.append(n) #res.add(n)
def get_sum(self,n:int) -> int:
n_str = str(n)
sum = 0
for i in n_str:
sum += int(i)**2
return sum
5.两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
#使用字典作为哈希表
record = {}
for idx,val in enumerate(nums):
#对nums中每个元素进行遍历
#判断每个元素之前与之前的元素之和是否为target
if target-val in record:
return [idx,record[target-val]]
record[val] = idx
return []
6.四数相加
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。
思路:
- 首先定义一个map,key存放a和b两数的和,value存放a和b两数之和出现的次数;
- 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中;
- 定义变量count,用来统计a+b+c+d=0出现的次数;
- 再遍历C和D数组,找到如果0-(c+d)出现在map中的情况,就用count把map中对应的value次数统计出来。
- 最后返回count就可以了。
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
map={}
for i in nums1:
for j in nums2:
if i+j in map:
map[i+j] += 1
else:
map[i+j]=1
count = 0
for i in nums3:
for j in nums4:
targrt = 0-i-j
if targrt in map:
count+=map[targrt]
return count
时间复杂度:O(n^2)
空间复杂度:O(n^2)
7.赎金信
给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。
(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
#只有26个字母,使用数组即可
str_count = [0]*26
for st in ransomNote:
str_count[ord(st) - ord('a')] += 1
for st in magazine:
str_count[ord(st) - ord('a')] -= 1
#magazine的字母可以比ransomNote的字母多,所以只要把str_count都变成0或者负数就可以
for i in str_count:
if i>0:
return False
return True
8.三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
思路:哈希解法:两层for循环可以确定a+b,再利用0-a-b来判断c,主要问题是去重,需要重新定义一个变量并且判断是否重复,非常费时且容易超时。
三指针法:先排序,使用三个指针遍历所有可能性
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
res = []
nums.sort() #从小到大排序
n=len(nums)
#三指针变量所有情况:cur left right
for cur in range(n):
if nums[cur]>0: #最小的大于0,那么就不可能和等于0
return res
if cur>0 and nums[cur]==nums[cur-1]: #相同的数就不需要再遍历了(去重)
continue
left = cur+1
right = n-1
while left<right:
total = nums[cur]+nums[left]+nums[right]
if total>0:
right-=1
elif total<0:
left+=1
else:
res.append([nums[cur],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 res
时间复杂度O(n^2)
空间复杂度O(1)
9.四数之和
题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:答案中不可以包含重复的四元组。
示例: 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2]
思路:与三数之和一样,先排序,然后使用四个指针遍历所有情况,并且在遍历过程中去掉重复的组合
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
nums.sort()
res = []
n = len(nums)
for i in range(n):
if nums[i]>target:
return res
if i>0 and nums[i]==nums[i-1]: #对i去重
continue
for j in range(i+1,n):
if j>i+1 and nums[j]==nums[j-1]:#对j去重
continue
left = j+1
right = n-1 #四个指针i,j,left,right
while left<right:
total = nums[left]+nums[right]+nums[i]+nums[j]
if total>target:
right-=1
elif total<target:
left+=1
else:
res.append([nums[i],nums[j],nums[left],nums[right]])
#对left 和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 res
10.总结
一般来说,哈希表是用来判断一个元素是否出现在集合里。
常用的三种哈希结构:
数组:固定了数值的长度,比如固定的26个字母
集合(set):计算两个列表的交并差
映射/字典(map):最常用
其他:三数之和四数之和需要去重时,哈希法去重很麻烦,不如使用指针法,先对数组进行sort排序,使用指针跳过下一个相同的数实现去重。