HashTable 哈希/散列表 面试基础

HashTable 哈希/散列表

1. 基本思想

哈希表也叫散列表,用于存储key-value键值对,通过将key映射到表中的一个位置来以常数的时间实现插入、删除和查找的技术。

使用哈希/散列函数将要查找的键值转化为数组的索引,在理想状态下,不同的键对应不同的索引,如果每个键对应的索引都不同的话,其实就是直接用数组存储。但如果key的范围很大的话,需要的数组太大(比如处理海量数据时的bitHash),内存消耗上不划算。一般来说,为了折中,用小于键范围的数组(容量为TableSize)来存储。此时,不同的键就会对应相同的索引,这个时候,需要解决哈希冲突的问题。

2. 哈希函数

对于整数来说,最简单的哈希函数就是 key mod TableSize,这个时候,保证table的大小为素数时,该哈希函数能够对随机整数实现平均分配。

下面是一些在应用领域较为出名的哈希算法:

  • MD5,信息摘要算法5,确保信息传输完整一致。MD5是输入不定长度信息,输出固定长度128bits的算法。
  • SHA-1,用于HTTPS传输和软件签名。
  • SHA-2:SHA-224/SHA-256/SHA-384/SHA-512并成为SHA-2
  • SHA-3:之前命名为Keccak算法,是一个加密杂凑算法。

3. 解决哈希冲突的方法

3.1 分离链接法(拉链法)

拉链法可用于避免哈希冲突,它的思想是把hash相同的元素存放在链表中,将数组和链表结合起来,平衡时间和空间,实现快速的查找插入和删除。

即,将数组中的每个元素指向一个链表,链表中的每一个节点都存储索引相同的键值对。拉链法的数组长度要足够大,使得链表尽量小,以保证查找效率。

在这里插入图片描述

可以指定散列表的最大装载因子 load factor,一旦超过就会rehashing。

load factor = 散列表中的元素个数与散列数组大小的比值。

一般设定为1,即所有存储的元素个数大于数组大小时,就rehashing。

3.2 线性探测法

与拉链法不同的是,该方法在检测到哈希冲突时,不拉出一个链表进行存储,而是逐个探测找出后序的空单元来解决冲突,如下图所示,插入Sandra dee时,因为152已被John smith占据,故放置于153,同理,插入Ted Baker时,虽然hash值为153,但因为被占据,就顺序放到154。

在这里插入图片描述
在线性探测法中,如果hash函数不均衡,很容易出现聚集的区块,为了保证效率,装填因子一般设为0.5,此时插入平均需要2.5次探测,成功查找平均需要1.5次探测。

3.3 平方探测法

为了解决线性探测法中一次聚集问题的冲突解决办法。平方探测就是先探测后序的第1个,如果被占用,则探测后序的第 2 2 = 4 2^2=4 22=4个,如果还被占用,探测后序的第9个。

装填因子一般低于0.5,即确保散列表大小至少是表中元素的两倍大,这样平方探测解法总可以实现。

3.4 双散列

最后一个解决冲突的办法是双散列,在计算探测位置时应用第二个哈希函数。当键为字符串时,由于散列函数的计算比较耗时,通常使用简单易行的平方探测法。

4. 扩充再散列rehash

如果散列表中的元素太多,超过设置的装填因子,那么操作的运行时间开始过长,此时进行rehash,建立一个新表(一般是原表大小两倍后的第一个素数),使用新的哈希函数扫描原表,将元素复制到新表里。

### 哈希表(散列表)的数据结构原理 哈希表是一种支持快速查找操作的数据结构,在理想条件下,其时间复杂度为 O(1)[^2]。这种高效性能源于哈希表内部采用了一种称为哈希函数的技术来映射键到数组中的位置。 #### 哈希表的概念 哈希表利用一种特殊的方式存储数据——不是按照顺序排列而是依据特定算法计算得到的位置放置元素。这种方式使得即使面对大量数据也能保持高效的存取速度[^1]。 #### 哈希函数设计原则 为了使哈希表能够正常工作并发挥最佳效能,需要精心设计哈希函数: - **均匀分布**:好的哈希函数应该尽可能让不同的输入产生不相同的输出值; - **简单快捷**:由于每次访问都要执行该函数,因此它应当具有较高的运算效率; - **减少冲突**:尽管完全消除碰撞是不可能的任务,但仍需努力降低发生概率; 一些常见的哈希函数构建方式包括但不限于直接定址法、除留余数法、平方取中法以及基数转换法等[^3]。 ```cpp // C++ 中简单的哈希函数示例 (除留余数法) int hashFunction(int key, int tableSize){ return key % tableSize; } ``` #### 处理哈希冲突的方法 当两个不同关键字经过相同哈希函数变换后指向同一索引时就会引发所谓的“哈希冲突”。针对这种情况有两种主要解决方案: ##### 开放地址法 这种方法是在遇到冲突时不创建新的节点链接起来形成链表,而是在原数组里寻找下一个可用槽位继续尝试插入直到成功为止。具体策略有线性探测、二次探查等多种变体形式。 ##### 链地址法(拉链法) 每当检测到已有记录占据目标位置,则新建一个单向链表连接这些溢出项。此方案相对容易实现且易于维护,但可能增加额外空间消耗。 ```cpp struct ListNode { int val; struct ListNode *next; }; class MyHashTable { public: vector<ListNode*> buckets; void insert(int value); }; ``` 对于 Java 的 `HashMap` 类而言,除了上述提到的基础机制外还涉及到了对象比较逻辑。每当涉及到新 Entry 加入或者旧 Entry 查找更新的时候都会先调用 `hashCode()` 方法获取对应 bucket 序号再进一步确认是否真的存在重复 Key 则会触发 `equals()` 进行最终判断[^4]。 ### 实际应用案例分析 在实际编程实践中,开发者可以根据需求选择合适的库或框架所提供的现成容器类型如 STL 中的 unordered_map 或者手写简易版自定义 HashTable 来满足项目所需功能特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值