Hash
google上搜索hash,得出的结果是Hash is a dish consisting of chopped meat, potatoes, and fried onions。翻译过来的意思是一道菜主要由切碎的肉、土豆和炒洋葱组成,它形象的描绘出比较零碎、杂乱的场景。
在计算机领域我们把hash一般称为哈希表,也称散列表,它由键值对(key-value)组成,是一种有广泛应用的数据结构。各类高级编程语言会提供相关api,如python中的字典(dict)、java中的HashMap等。
哈希表(Hash table),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做hash函数,存放记录的数组叫做散列表。
hash函数
在哈希表中,数据元素(key,value)的存放位置和数据元素的关键字(key)之间存在对应关系,建立这种对应关系的函数称为hash函数。key经过hash函数计算出的值为hash值,一般常说的hash code。
hash_code = func(key),每个key都会有对应的hash值(hash code),hash表希望hash函数计算出的hash code均匀散列,这样的结构会使得hash效果更优。
假设hash函数定义为:func(key) = key % 10,根据hash函数,可以求得key=1,9,11的hash code。
func(1) = 1;
func(9) = 9;
func(11) = 1;
key = 1 与 key = 11的hash code相同。
hash冲突
不同的key值由哈希函数计算出的哈希值相同时,此时会产生hash冲突。如上hash函数,在key = 1 与 key = 11时会产生hash冲突。
在不愿牺牲空间的情况下,完美的hash函数难找,但能想办法去解决hash冲突。下面介绍两个解决hash冲突的方法及其简单实现。
开放定址法
开放定址法也称线性探测法,就是从发生冲突的那个位置开始,按照一定次序从Hash表找到一个空闲位置然后把发生冲突的元素存入到这个位置。只要哈希表足够大,空的哈希地址总能找到,然后将数据存入该位置。通过这种办法避免hash冲突时,可以将hash表的内部结构看作是一个数组,数组的每个元素看作是一个数组。
# 创建一个开放定址法哈希表类
class OpenAddressingHashTable:
# 初始化函数,设置哈希表的大小
def __init__(self, size):
self.table = [None] * size # 创建一个指定大小的哈希表,所有位置初始为空
# 定义哈希函数,这里使用简单的取模作为哈希函数
def hash_function(self, key):
return key % len(self.table) # 返回键对表大小取模的结果
# 线性探测解决哈希冲突
def linear_probe(self, key):
index = self.hash_function(key) # 计算初始索引
while self.table[index] is not None: # 如果该位置已被占用
index = (index + 1) % len(self.table) # 线性探测,索引递增,到达末尾后回到开头
return index # 返回找到的空闲位置的索引
# 插入操作
def insert(self, key, value):
index = self.linear_probe(key) # 寻找插入位置
self.table[index] = (key, value) # 在找到的空闲位置插入(键,值)对
# 搜索操作
def search(self, key):
index = self.hash_function(key) # 计算键的初始索引
while self.table[index] is not None: # 如果当前位置不为空,继续搜索
if self.table[index][0] == key: # 如果找到了键
return self.table[index][1] # 返回对应的值
index = (index + 1) % len(self.table) # 没找到,线性探测下一个位置
return None # 如果没有找到键,返回None
# 示例使用
hash_table = OpenAddressingHashTable(10) # 创建一个大小为10的哈希表
hash_table.insert(1, "A") # 插入键值对(1, "A")
hash_table.insert(11, "B") # 插入键值对(11, "B"),演示哈希冲突的处理
print(hash_table.search(1)) # 输出: A
print(hash_table.search(11)) # 输出: B
链地址法
链地址法也称拉链法,当冲突发生时,冲突的元素将被加到该位置链表的最后。通过这种办法避免hash冲突时,可以将hash表的内部结构看作是一个数组,数组的每个元素看作是一个链表。
class HashTable:
def __init__(self, size=10):
self.size = size
# 初始化哈希表,每个桶(bucket)开始时都是一个空链表
self.table = [[] for _ in range(self.size)]
def hash_function(self, key):
# 简单的哈希函数:键对大小取模
return key % self.size
def insert(self, key, value):
# 插入元素到哈希表中
index = self.hash_function(key) # 计算键的哈希值
bucket = self.table[index] # 获取对应的桶
for i, (k, v) in enumerate(bucket):
if k == key:
# 如果键已经存在,更新其值
bucket[i] = (key, value)
return
bucket.append((key, value)) # 将键值对添加到链表的末尾
def search(self, key):
# 搜索键对应的值
index = self.hash_function(key) # 计算键的哈希值
bucket = self.table[index] # 获取对应的桶
for k, v in bucket:
if k == key:
# 如果找到了键,则返回对应的值
return v
return None # 如果没有找到键,返回None
def delete(self, key):
# 删除键
index = self.hash_function(key) # 计算键的哈希值
bucket = self.table[index] # 获取对应的桶
for i, (k, _) in enumerate(bucket):
if k == key:
# 如果找到了键,则删除这个元素
del bucket[i]
return True
return False # 如果没有找到键,返回False
# 示例使用
hash_table = HashTable()
hash_table.insert(1, "Alice")
hash_table.insert(2, 30)
print(hash_table.search(1)) # 输出: Alice
print(hash_table.search(2)) # 输出: 30
hash_table.delete(1)
print(hash_table.search(1)) # 输出: None
扩容与rehash
当哈希表的填充因子(已存储的键值对数量与哈希表大小的比值)达到某个阈值时,扩容(增加哈希表的大小)和重新哈希(rehash)是必要的步骤,以保持哈希表的操作效率。这个过程中,哈希表中现有的键值对(key, value)数据需要根据键值(key)都需要重新计算哈希值并分配到新的哈希表中。
class HashTable:
def __init__(self):
self.size = 8
self.count = 0
self.table = [[] for _ in range(self.size)]
def hash_function(self, key):
return hash(key) % self.size
def rehash(self, new_size):
old_table = self.table
self.size = new_size
self.table = [[] for _ in range(self.size)]
self.count = 0 # 重置计数器
for bucket in old_table:
for key, value in bucket:
self.insert(key, value) # 将旧表中的数据重新哈希到新表中
def resize_check(self):
# 填充因子超过阈值(这里简单设为0.75),则扩容
if self.count / self.size > 0.75:
self.rehash(2 * self.size) # 扩容到当前大小的2倍
def insert(self, key, value):
self.resize_check() # 在每次插入前检查是否需要扩容和rehash
index = self.hash_function(key)
bucket = self.table[index]
for i, (k, _) in enumerate(bucket):
if k == key:
bucket[i] = (key, value) # 已存在则更新
return
bucket.append((key, value)) # 不存在则添加
self.count += 1
def search(self, key):
index = self.hash_function(key)
bucket = self.table[index]
for k, v in bucket:
if k == key:
return v
return None
def delete(self, key):
index = self.hash_function(key)
bucket = self.table[index]
for i, (k, _) in enumerate(bucket):
if k == key:
del bucket[i]
self.count -= 1
return True
return False
# 示例使用
hash_table = HashTable()
for i in range(10):
hash_table.insert(f'key{i}', f'value{i}')
print(hash_table.search('key5')) # 输出: value5
hash_table.delete('key5')
print(hash_table.search('key5')) # 输出: None
优缺点
优点:非常适合指定查找,也适合插入和删除
缺点:依赖优秀的hash函数
应用
从上面例子的描述里可以得知,数组是实现hash的基础,用数组作为底层结构实现了hash表,这是实现方式的一种,并不能代表实际应用。在实际应用中场景是复杂多变的,比如python语言中的hash表的典型应用是字典(dict),字典底层是2个数组。
# 假设是一个空hash表初始如下
enteies = []
indices = [None, None, None, None, None, None, None, None]
#(key0, value0)放入hash表
hash_value0 = hash('key0') # 假设值为 hash0
# 将 [hash0, key0, value0]放入enteies
enteies = [
[hash0, key0, value0]
]
index0 = hash_value0 & ( len(indices) - 1) # 假设index0值计算后等于3
# 并将此元素在enteies下标位置0记录到indices[index0]
indices = [None, None, None, 0, None, None, None, None]
#(key1, value1)放入hash表
hash_value1 = hash('key1') # 假设值为 hash1
# 将 [hash1, key1, value1]放入enteies
enteies = [
[hash0, key0, value0],
[hash1, key1, value1]
]
index1 = hash_value1 & ( len(indices) - 1) # 假设index1值计算后等于1
# 并将此元素在enteies下标位置1记录到indices[index1]
indices = [None, 1, None, 0, None, None, None, None]
2的i次方大小的表,右边的下标j发生hash冲突时,线性探测之伪随机重新计算得到左边的下标j
# python-3.12 字典源码中找到的一个公式 线性探测之伪随机
j = ((5*j) + 1) mod 2**i
"""
i=3时,j=0 -> 1 -> 6 -> 7 -> 4 -> 5 -> 2 -> 3 -> 0
1 = ((5*0) + 1) mod 2**3
6 = ((5*1) + 1) mod 2**3
7 = ((5*6) + 1) mod 2**3
4 = ((5*7) + 1) mod 2**3
5 = ((5*4) + 1) mod 2**3
2 = ((5*5) + 1) mod 2**3
3 = ((5*2) + 1) mod 2**3
0 = ((5*3) + 1) mod 2**3
"""
class SimpleHashTable:
def __init__(self):
self.num = 3
self.size = 2 ** self.num
self.count = 0
self.table = [[] for _ in range(self.size)]
def hash_function(self, key):
"""
hash函数,使用取模运算
:param key: 键
:return:
"""
return hash(key) % self.size
def rehash(self):
"""
重新hash,一般在扩容后需要重新hash
:return:
"""
old_table = self.table
self.num += 1
self.size = 2 ** self.num
self.table = [[] for _ in range(self.size)]
self.count = 0 # 重置计数器
for bucket in old_table:
for key, value in bucket:
self.insert(key, value) # 将旧表中的数据重新哈希到新表中
def resize_check(self):
"""
检查是否需要扩容
:return:
"""
# 填充因子超过阈值(这里简单设为0.75),则扩容
if self.count / self.size > 0.75:
self.rehash() # 扩容到当前大小的2倍
def insert(self, key, value):
"""
hash表插入
:param key: 键
:param value: 值
:return:
"""
self.resize_check() # 在插入前检查是否需要扩容和rehash
index = self.hash_function(key)
if self.table[index] is not None and self.table[index] != []:
# 如果当前位置已经有数据,判断key是否相等 相等则更新value 不相等则使用伪随机探测出新的index
for k, v in self.table[index]:
if k == key:
self.table[index] = [(key, value)]
return
index = self.pseudo_random_sequence(index)
self.table[index] = [(key, value)]
self.count += 1
def pseudo_random_sequence(self, index: int):
"""
伪随机探测 找到一个空位
:param index:冲突位
:return: 空位
"""
next_index = ((5 * index) + 1) % self.size
while self.table[next_index] is not None and self.table[next_index] != []:
next_index = ((5 * next_index) + 1) % self.size
return next_index
def search(self, key):
index = self.hash_function(key)
bucket = self.table[index]
for k, v in bucket:
if k == key:
return v
return None
def delete(self, key):
"""
删除key
:param key:key
:return:删除成功-True,否则-False
"""
index = self.hash_function(key)
buckets = self.table[index]
if buckets is not None and buckets != []:
bucket = buckets[0]
k, v = bucket
if k == key:
self.table[index] = [] # 重置为空
self.count -= 1
return True
return False
# 示例使用
hash_table = SimpleHashTable()
hash_table.insert(11, 11)
print(hash_table.table)
for i in range(10):
hash_table.insert(i, i)
print(hash_table.table)
print(hash_table.search(5)) # 输出: value5
hash_table.delete(5)
print(hash_table.table)
print(hash_table.search(5)) # 输出: None
扩容时生成新数组,是否可以考虑惰性转移