哈希表理论基础
哈希表都是用来快速判断一个元素是否出现集合里,如果枚举查找,需要O(n),哈希表索引查找,只要O(1)。
- 哈希函数:将内容映射为哈希表的索引
- 哈希碰撞是指在哈希表中,不同的键通过哈希函数产生了相同的哈希值。由于哈希表使用哈希值来确定键值对存储的位置,所以哈希碰撞需要妥善处理,以避免数据丢失或查找错误。
处理哈希碰撞的常见方法
-
开放地址法(Open Addressing)
- 当发生碰撞时,寻找下一个空闲的存储位置。
- 常见策略:
- 线性探测(Linear Probing):如果发生碰撞,则检查下一个位置,依次类推,直到找到空闲位置。
- 二次探测(Quadratic Probing):如果发生碰撞,检查 (i^2) 位置(i是碰撞次数),减少聚集现象。
- 双重哈希(Double Hashing):使用第二个哈希函数计算新的存储位置。
-
链地址法(Chaining)
- 每个存储位置维护一个链表,所有产生相同哈希值的键值对都存储在该链表中。
- 当发生碰撞时,将新的键值对插入到链表中。
示例代码
以下是使用链地址法(Chaining)处理哈希碰撞的简单示例代码:
class HashTable:
def __init__(self, size):
self.size = size
self.table = [[] for _ in range(size)]
def hash_function(self, key):
return hash(key) % self.size
def insert(self, key, value):
index = self.hash_function(key)
for kvp in self.table[index]:
if kvp[0] == key:
kvp[1] = value
return
self.table[index].append([key, value])
def search(self, key):
index = self.hash_function(key)
for kvp in self.table[index]:
if kvp[0] == key:
return kvp[1]
return None
def delete(self, key):
index = self.hash_function(key)
for i, kvp in enumerate(self.table[index]):
if kvp[0] == key:
del self.table[index][i]
return
# 示例使用
ht = HashTable(10)
ht.insert("apple", 1)
ht.insert("banana", 2)
ht.insert("orange", 3)
print(ht.search("apple")) # 输出: 1
print(ht.search("banana")) # 输出: 2
print(ht.search("grape")) # 输出: None
ht.delete("banana")
print(ht.search("banana")) # 输出: None
不同处理方法的优缺点
-
开放地址法:
- 优点:不需要额外的存储空间,适合较小的哈希表。
- 缺点:当负载因子接近1时,查找效率显著下降,需要处理删除操作的复杂性。
-
链地址法:
- 优点:即使负载因子高时,查找效率也相对稳定,删除操作简单。
- 缺点:需要额外的存储空间来维护链表,当很多元素产生相同哈希值时,链表会变得很长。
选择适当的方法
- 当哈希表较大且冲突概率较低时,开放地址法可以更节省空间。
- 当哈希表较小或负载因子较高时,链地址法能更好地处理碰撞并保持查找效率。
数据结构
set
、list
和 map
(在Python中对应 dict
)在哈希表应用中有以下主要区别:
对比
特性 | set | list | dict |
---|---|---|---|
存储内容 | 不重复元素 | 有序且可重复元素 | 键值对 |
实现方式 | 哈希表 | 动态数组 | 哈希表 |
有序性 | 无序(无特定顺序) | 有序(按插入顺序) | 无序(Python 3.7 之前) 有序(Python 3.7 及之后) |
重复性 | 不允许重复 | 允许重复 | 键不允许重复 |
查找复杂度 | O(1) | O(n) | O(1) |
应用场景 | 快速检查元素是否存在 去重 | 保留元素顺序 允许重复元素 | 快速查找、插入和删除 基于键的值 |
- 使用
set
来处理无序且不重复的元素。 - 使用
list
来处理有序且允许重复的元素。 - 使用
dict
来处理键值对并需要快速查找值的场景。
题目
242.有效的字母异位词
用collections.Counter来计数,返回的是一个字典
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
# 用collections.Counter来计数,返回的是一个字典
return Counter(s) == Counter(t)
ord()返回单个字符对应的 Unicode 编码,用数组,索引可以用a~z的 ASCII 值,字符减去a,用两个数组可以优化成一个数组,先加计算s的,再减去t的,看是否等于0,看是否是空数组 生成器+all, 类似的还有any(),(是否有true) filter()(找出所有true的元素,常与lamba连用)
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
# 用数组,索引可以用a~z的 ASCII 值,字符减去a
# 用两个数组可以优化成一个数组,先加计算s的,再减去t的,看是否等于0
cnt = [0]*26
for c in s:
cnt[ord(c) - ord('a')] += 1
for c in t:
cnt[ord(c) - ord('a')] -= 1
return all(n == 0 for n in cnt)# 看是否是空数组 生成器+all, 类似的还有any() \filter()
349. 两个数组的交集
无序,去重交集(都出现)。
如果数据量很大,用列表明显不合适,下标放不了那么大的数,用set
什么时候不用列表:(1)数据量大,下标放不下(2)数据分散,因为下标是连续的
法一:set
交集 & 并集 | 差集 -
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
return list(set(nums1)&set(nums2))
法二:字典 dict
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
table = {}
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] 由于set是不重复,所以不del也可以
# print(res)
# print(list(res)) # 集合直接变成列表
return list(res)
202. 快乐数
即使数字很大,不断平方和之后也会小下来回到 [1,243] 这个范围内,所以是一个环形链表的问题。最终只有:达到循环某个数 or 循环等于1(即快乐数)
注意 函数要么在方法内部定义,定义在引用之前,要么在类内定义为方法,用self.function()调用
class Solution:
def isHappy(self, n: int) -> bool:
fast = n
slow = n
def sumNext(num: int) -> int:
sum = 0
temp = num
while temp > 0:
dig = temp % 10
temp = temp // 10
sum += dig**2
return sum
# 一直循环,有出现就跳出
while True:
fast = sumNext(fast)
fast = sumNext(fast)
slow = sumNext(slow)
if fast == 1:
return True
if fast == slow:
return False
1. 两数之和
找到了一个数,要看他的搭子有没有出现过,(在一些数中找一个数sum-num有没有)看有咩有出现过就用哈希表的方法
数不连续 pass list,集合没有办法找到下标 pass,字典 可以,key是数,value是下标。
map存放的是遍历过的元素。
暴力做法每次拿两个数出来相加,和 target 比较,那么花费 O(1) 的时间,只获取了 O(1) 的信息。 而哈希表做法,每次查询都能知道 O(n) 个数中是否有 target−nums[j],那么花费 O(1) 的时间,就获取了 O(n) 的信息。
注意
- 判断字典里有没有 用if x in map,如果没有x在map,map[x]报错
字典添加元素 map[x] = index - 判断集合有没有 if x in setA
集合添加元素 setA.add(x) - 两者初始化都是 = { }
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
map = {}
for idx, x in enumerate(nums): # 遍历下标和数值
if target - x in map:
return [map[target - x], idx]
map[x] = idx