【算法】哈希表
1 哈希表理论基础
1.1 哈希表
哈希表是根据关键码的值而直接进行访问的数据结构。一般哈希表都是用来快速判断一个元素是否出现集合里。
1.2 哈希函数
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值。如果hashCode得到的数值大于tableSize,此时为了保证映射出来的索引数值都落在哈希表上,会再次对数值做一个取模的操作。
好几个名字同时映射到哈希表同一个索引下标的位置,叫哈希碰撞。
一般有两种解决方法, 拉链法和线性探测法。
拉链法
发生冲突的元素都被存储在链表中。拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法
使用线性探测法,一定要保证tableSize大于dataSize,依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置放了小李,那么就向下找一个空位放置小王的信息,如图所示:
1.3 常见的三种哈希结构
- 数组
- set (集合)
- map(映射)
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
map 是一个key-value的数据结构,map中对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的。
1.4 总结
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
2 有效的字母异位词*
题目:给定两个字符串 s
和 t
,编写一个函数来判断 t
是否是 s
的字母异位词。
注意:若 s
和 t
中每个字符出现的次数都相同,则称 s
和 t
互为字母异位词。
说明: 字符串只包含小写字母。
示例 1:
输入:s = "anagram",t = "nagaram"
输出: true
示例 2:
输入:s = "rat",t = "car"
输出:false
思路
1. 数组
数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个大小为26的数组record,来记录字符串s里字符出现的次数。需要把字符映射到数组索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。
在遍历字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,这样就将字符串s中字符出现的次数统计出来了。同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
record = [0] * 26
for i in s:
#并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
record[ord(i) - ord("a")] += 1
for i in t:
record[ord(i) - ord("a")] -= 1
for i in range(26):
if record[i] != 0:
#record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return False
return True
时间复杂度: O(n),空间复杂度: O(1)
2. defaultdict
defaultdict使得在处理字典时更加方便,无需在访问不存在的键之前检查键是否存在,而是可以直接使用它们。
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
from collections import defaultdict
# 创建一个 defaultdict,指定默认值为 int 类型,即默认值为 0
s_dict = defaultdict(int)
t_dict = defaultdict(int)
for x in s:
s_dict[x] += 1 # 访问不存在的键不会引发 KeyError,而是使用默认值 0 创建该键,可以在不提前设置默认值的情况下递增不存在的键的值
for x in t:
t_dict[x] += 1
return s_dict == t_dict
3. Counter
Counter
是 Python 中 collections
模块中的一个类,它用于计数可哈希对象(hashable objects)的出现次数。Counter
返回一个字典,其中键是可哈希对象,而值是该对象在输入序列中出现的次数。Counter
是一种非常方便的工具,特别适用于统计元素的频率。
class Solution(object):
def isAnagram(self, s: str, t: str) -> bool:
from collections import Counter
a_count = Counter(s)
b_count = Counter(t)
return a_count == b_count
3 两个数组的交集
题目:给定两个数组 nums1
和 nums2
,返回它们的交集 。输出结果中的每个元素一定是唯一的。我们可以不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
思路
要求不重复,可以用set
1. 使用字典和集合
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
from collections import defaultdict
dict = defaultdict(int) # 使用哈希表存储一个数组中的所有元素
for i in nums1:
dict[i] += 1
union = set() # 使用集合存储结果
for i in nums2:
if i in dict:
union.add(i)
return list(union) # 记得转回list
时间复杂度:O(n),空间复杂度:O(n)
2. 使用集合*
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
return list(set(nums1) & set(nums2))
tips:
- 交集操作:
&
,用于获取两个集合的共同元素,返回一个新集合,包含两个集合中都存在的元素。 - 并集操作:
|
,表示两个集合的合并,返回包含两个集合中所有不重复元素的新集合。 - 差集操作:
-
,用于从一个集合中移除另一个集合中包含的元素,返回一个新集合,包含在第一个集合中但不在第二个集合中的元素。 - 对称差集操作:
^
,用于获取两个集合中不重复的元素,返回一个新集合,包含只存在于一个集合中的元素。
思考:为什么不直接所有题目都用set?
直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算。不要小瞧这个耗时,在数据量大的情况,差距是很明显的。
4 快乐数*
题目:编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n
是 快乐数 就返回 true
;不是,则返回 false
。
示例 1:
输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
思路**
题目中说了会无限循环,也就是说求和的过程中,sum会重复出现,这对解题很重要。
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。
所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
1. 集合解法一(divmod
class Solution:
def isHappy(self, n: int) -> bool:
record = set()
while True:
n = self.get_sum(n)
if n == 1:
return True
# 如果中间结果重复出现,说明陷入死循环了,该数不是快乐数
if n in record:
return False
else:
record.add(n)
def get_sum(self,n: int) -> int:
new_num = 0
while n:
n, r = divmod(n, 10)
new_num += r ** 2
return new_num
2. 集合解法二(字符串↔int
class Solution:
def isHappy(self, n: int) -> bool:
record = set()
while n not in record:
record.add(n)
new_num = 0
n_str = str(n)
for i in n_str:
new_num+=int(i)**2
if new_num==1: return True
else: n = new_num
return False
3. 集合解法三(精简**
class Solution:
def isHappy(self, n: int) -> bool:
seen = set()
while n != 1:
n = sum(int(i) ** 2 for i in str(n))
if n in seen:
return False
seen.add(n)
return True
5 两数之和
题目:给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 2:
输入:nums = [3,3], target = 6
输出:[0,1]
思路
本题需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是是否出现在这个集合。因为本题不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
1. 字典法
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
records = dict()
for index, value in enumerate(nums):
if target - value in records: # 遍历当前元素,并在map中寻找是否有匹配的key
return [records[target- value], index]
records[value] = index # 如果没找到匹配对,就把访问过的元素和下标加入到map中
return []
2. 集合法
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
seen = set() #创建一个集合来存储我们目前看到的数字
for idx, val in enumerate(nums):
if target - val in seen:
return [nums.index(target - val), idx]
seen.add(val)
时间复杂度:O(n),空间复杂度:O(n)
6 四数相加
题目:给你四个整数数组 nums1
、nums2
、nums3
和 nums4
,数组长度都是 n
,请你计算有多少个元组 (i, j, k, l)
能满足:nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
思路*
本题解题步骤:
- 首先定义一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
- 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
- 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
- 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
- 最后返回统计值 count 就可以了
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
dict, cnt = defaultdict(int), 0
for i in nums1:
for j in nums2:
dict[i + j] += 1 # 初始默认是0
for i in nums3:
for j in nums4:
cnt += dict[-(i + j)] # 没有的话就是0
return cnt
时间复杂度:O(n^2),空间复杂度:O(n^2)
7 赎金信
题目:给你两个字符串:ransomNote
和 magazine
,判断 ransomNote
能不能由 magazine
里面的字符构成。
如果可以,返回 true
;否则返回 false
。
magazine
中的每个字符只能在 ransomNote
中使用一次。
示例 1:
输入:ransomNote = "a", magazine = "b"
输出:false
示例 2:
输入:ransomNote = "aa", magazine = "aab"
输出:true
思路
1. defaultdict法
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
dict = defaultdict(int)
for s in magazine:
dict[s] += 1
for s in ransomNote:
dict[s] -= 1
# for val in dict.values():
# if val < 0:
# return False
# return True
return all(val >= 0 for val in dict.values())
2. Counter法*
from collections import Counter
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
return not Counter(ransomNote) - Counter(magazine)
3. count法*
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
return all(ransomNote.count(c) <= magazine.count(c) for c in set(ransomNote))
8 三数之和*
题目:给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为 0
且不重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
思路
1. 哈希法
去重很容易超时!需要剪枝(有点恶心就跳过吧)
2. 双指针法
这题双指针法比哈希法高效一点。首先将数组排序,有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。
接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0 说明此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
# res = set()
for i in range(len(nums) - 2):
left, right = i + 1, len(nums) - 1
# 剪枝
if nums[i] > 0:
break
elif nums[i] + nums[left] > 0:
break
if i > 0 and nums[i] == nums[i - 1]: # a去重
continue
while left < right:
if nums[i] + nums[left] + nums[right] < 0:
left += 1
elif nums[i] + nums[left] + nums[right] > 0:
right -= 1
else:
res.append([nums[i], nums[left], nums[right]])
# 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
# res.add((nums[i], nums[left], nums[right]))
left += 1
right -= 1
return res
# return list(res)
9 四数之和**(经典
题目:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
思路**
注意细节:不要判断nums[k] > target
就返回了,三数之和可以通过 nums[i] > 0
就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2, -1]
,target
是-10
,不能因为-4 > -10
而跳过。
1. 双指针法
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
nums.sort()
res = []
for i in range(len(nums)):
if i > 0 and nums[i] == nums[i - 1]: # a去重
continue
if nums[i] > target and nums[i] >= 0: # 剪枝(optional
break
for j in range(i+1, len(nums)):
left, right = j + 1, len(nums) - 1
sum = nums[i] + nums[j]
if sum > target and sum >= 0: # 剪枝(optional
break
if j > i + 1 and nums[j] == nums[j - 1]: # b去重
continue
while left < right:
if sum + nums[left] + nums[right] < target:
left += 1
elif sum + nums[left] + nums[right] > target:
right -= 1
else:
res.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
return res
2. 字典法(适用于n数之和题目/要求输出为值*
class Solution(object):
def fourSum(self, nums, target):
# 创建一个字典来存储输入列表中每个数字的频率
freq = defaultdict(int)
for i in nums:
freq[i] += 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]
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧