Hash_温故知新

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

扩容时生成新数组,是否可以考虑惰性转移

  • 13
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值