哈希算法题目:设计哈希映射(线性探测法实现)
题目描述:你需要设计一个哈希映射(HashMap),使用开放地址法中的线性探测策略来解决哈希冲突。这个哈希映射应该支持以下操作:
put(key, value):插入一个键值对。如果键已存在,则更新其对应的值。get(key):返回键对应的值。如果键不存在,返回 -1。remove(key):删除键及其对应的值。
要求:使用一个固定大小的数组来存储键值对,不依赖于内置的哈希表库。你需要处理哈希冲突,并通过线性探测在数组中寻找下一个可用位置。
解题步骤讲解:
步骤1:理解哈希映射和线性探测的基本概念
哈希映射(HashMap)是一种基于键(key)直接访问值(value)的数据结构。核心思想是:通过一个哈希函数将键映射到一个索引位置,然后将键值对存储在该索引对应的数组位置。
然而,当两个不同的键被哈希函数映射到同一个索引时,就会发生“哈希冲突”。开放地址法是解决冲突的一种策略:当冲突发生时,探测(寻找)数组中的其他位置,直到找到一个空位或满足特定条件的位置。
线性探测是开放地址法中最简单的一种:当位置i(索引)被占用时,就尝试下一个位置 i+1,然后是 i+2,以此类推,直到找到一个空位。查找时也遵循同样的探测顺序。
步骤2:设计数据结构
我们将使用一个固定大小的数组来存储键值对。但注意:我们需要区分“空位置”和“被删除的位置”,因为在删除后,线性探测的查找链不能中断。所以,每个数组元素可以有三个状态:
- 空(从未使用过)
- 占用(已存储一个键值对)
- 已删除(曾存储过,但已被删除)
我们可以用两个数组来实现:一个存储键,一个存储值,并用一个额外的状态数组记录每个位置的状态。但为简化,我们可以用一个数组存储特殊的“单元”,每个单元包含 key、value 和状态。
为简单讲解,我们假设键和值都是整数。我们初始化一个固定大小的数组,每个位置包含 (key, value) 对,并设置一个状态标志。我们将状态分为:
EMPTY:该位置为空。OCCUPIED:该位置有有效的键值对。DELETED:该位置曾被占用,但已被删除。
步骤3:确定基本参数
我们需要定义:
- 数组容量(capacity):固定大小,比如 10007(一个质数,有助于减少聚集)。
- 哈希函数:
hash(key) = key % capacity。确保结果在 [0, capacity-1] 范围内。 - 线性探测步长:每次探测索引加 1(即 index = (index + 1) % capacity),循环检查直到满足条件。
步骤4:详细操作逻辑
a) 插入操作 put(key, value)
- 计算初始索引:
index = key % capacity。 - 从 index 开始,顺序向后探测(循环数组):
- 如果遇到状态为
EMPTY或DELETED的位置,就将键值对存入,并将状态设为OCCUPIED,然后返回。 - 如果遇到状态为
OCCUPIED且键相等,就更新其值,然后返回。
- 如果遇到状态为
- 注意:如果数组满了,此简单实现会无限循环,实际中需要处理扩容,但题目要求固定大小,所以我们假设数组不会满。
b) 查找操作 get(key)
- 计算初始索引:
index = key % capacity。 - 从 index 开始,顺序向后探测(循环数组):
- 如果遇到状态为
EMPTY,说明键不存在,返回 -1。 - 如果遇到状态为
OCCUPIED且键相等,就返回值。 - 如果遇到状态为
DELETED或其他键,就继续探测。
- 如果遇到状态为
- 如果循环回起点还没找到,就返回 -1。
c) 删除操作 remove(key)
- 计算初始索引:
index = key % capacity。 - 从 index 开始,顺序向后探测(循环数组):
- 如果遇到状态为
EMPTY,说明键不存在,直接返回。 - 如果遇到状态为
OCCUPIED且键相等,就将状态改为DELETED(注意:只改状态,不清空键值,以保持探测链,但标记为可覆盖),然后返回。 - 如果遇到状态为
DELETED或其他键,就继续探测。
- 如果遇到状态为
步骤5:代码结构示例(伪代码)
定义常量:EMPTY, OCCUPIED, DELETED
定义容量 capacity = 10007
定义数组 keys[capacity], values[capacity], status[capacity] 初始化为 EMPTY
函数 hash(key):
return key % capacity
函数 put(key, value):
index = hash(key)
while status[index] != EMPTY:
if status[index] == OCCUPIED 且 keys[index] == key:
values[index] = value
return
index = (index + 1) % capacity
keys[index] = key
values[index] = value
status[index] = OCCUPIED
函数 get(key):
index = hash(key)
while status[index] != EMPTY:
if status[index] == OCCUPIED 且 keys[index] == key:
return values[index]
index = (index + 1) % capacity
return -1
函数 remove(key):
index = hash(key)
while status[index] != EMPTY:
if status[index] == OCCUPIED 且 keys[index] == key:
status[index] = DELETED
return
index = (index + 1) % capacity
步骤6:关键细节与注意事项
- 循环数组:探测时索引达到数组末尾后,应回到开头,使用取模运算
(index + 1) % capacity。 - 删除标记:删除时不能直接设为
EMPTY,否则会中断后续元素的探测链。标记为DELETED后,插入时可以被重用,查找时会跳过。 - 查找终止条件:查找时遇到
EMPTY就停止,因为线性探测保证元素不可能在更后面。 - 聚集问题:线性探测容易产生“一次聚集”(连续占用的位置形成长块),导致效率下降。这是其缺点,但实现简单。
步骤7:复杂度分析
- 插入、删除、查找的平均时间复杂度:在良好的哈希函数和负载因子下,为 O(1)。最坏情况(全聚集)为 O(n)。
- 空间复杂度:O(capacity),固定大小。
通过以上步骤,你就能设计一个基于线性探测的哈希映射了。这个实现是开放地址法的经典示例,重点在于理解线性探测的机制和删除标记的重要性。