1. 题目
不使用任何内建的哈希表库设计一个哈希映射(HashMap
)。
实现 MyHashMap
类:
MyHashMap()
用空映射初始化对象;void put(int key, int value)
向HashMap
插入一个键值对(key, value)
。如果key
已经存在于映射中,则更新其对应的值value
;int get(int key)
返回特定的key
所映射的value
;如果映射中不包含key
的映射,返回 − 1 -1 −1 。
void remove(key)
如果映射中存在key
的映射,则移除key
和它所对应的value
。
1.1 示例
输入:
["MyHashMap", "put", "put", "get", "get", "put", "get", "remove", "get"]
[[], [1, 1], [2, 2], [1], [3], [2, 1], [2], [2], [2]]
输出:
[null, null, null, 1, -1, null, 1, null, -1]
解释:
MyHashMap myHashMap = new MyHashMap();
myHashMap.put(1, 1); // myHashMap 现在为 [[1,1]]
myHashMap.put(2, 2); // myHashMap 现在为 [[1,1], [2,2]]
myHashMap.get(1); // 返回 1 ,myHashMap 现在为 [[1,1], [2,2]]
myHashMap.get(3); // 返回 -1(未找到),myHashMap 现在为 [[1,1], [2,2]]
myHashMap.put(2, 1); // myHashMap 现在为 [[1,1], [2,1]](更新已有的值)
myHashMap.get(2); // 返回 1 ,myHashMap 现在为 [[1,1], [2,1]]
myHashMap.remove(2); // 删除键为 2 的数据,myHashMap 现在为 [[1,1]]
myHashMap.get(2); // 返回 -1(未找到),myHashMap 现在为 [[1,1]]
1.2 说明
- 来源: 力扣(LeetCode)
- 链接: https://leetcode-cn.com/problems/design-hashmap
1.3 限制
0 <= key, value <= 106
- 最多调用
1
0
4
10^4
104 次
put
、get
和remove
方法
2. 解法一(分离链接法)
2.1 分析
详细分析请参考:【数据结构Python描述】仿照Python解释器使用哈希表手动实现一个字典
下面首先使用列表作为每个 bucket
发生哈希碰撞时所使用的二级容器。
2.2 解答
from random import randrange
class MyHashMap:
class _Item:
__slots__ = 'key', 'value'
def __init__(self, key, value):
self.key = key
self.value = value
def __init__(self, cap=6500, p=109345121):
"""创建一个空的映射"""
self._table = cap * [[]]
self._n = 0
self._prime = p # MAD压缩函数中大于哈希表容量的大质数
self._scale = 1 + randrange(p-1) # MAD压缩函数中的缩放系数a
self._shift = randrange(p) # MAD压缩函数中的偏移系数b
def _hash_function(self, key):
"""哈希函数"""
return (self._scale * hash(key) + self._shift) % self._prime % len(self._table)
def __len__(self):
return self._n
def get(self, key):
j = self._hash_function(key)
bucket = self._table[j]
for item in bucket:
if item.key == key:
return item.value
return -1
def put(self, key, value):
j = self._hash_function(key)
size = len(self._table[j])
for item in self._table[j]: # 遍历查询映射中是否存在键key
if key == item.key:
item.value = value
return
self._table[j].append(self._Item(key, value)) # 映射中不存在键key
if len(self._table[j]) > size: # k为新的键
self._n += 1
if self._n > len(self._table) // 2: # 确保负载系数小于0.5
self._resize(2 * len(self._table) - 1) # 通常2 * n - 1为质数
def remove(self, key):
j = self._hash_function(key)
bucket = self._table[j]
for item in bucket: # 遍历查询映射中是否存在键key
if key == item.key:
bucket.remove(item)
self._n -= 1
return
def _resize(self, cap):
"""将哈希表容量调整为cap"""
old = list(self) # 通过迭代获得已有的所有键值对
self._table = cap * [[]]
self._n = 0
for key, value in old:
self.put(key, value)
def __iter__(self):
for bucket in self._table:
if bucket is not None:
for item in bucket:
yield item.key, item.value
下面使用链表作为每个 bucket
发生哈希碰撞时所使用的二级容器。
from random import randrange
class _ListNode:
def __init__(self, key=0, value=0, next=None):
self.key = key
self.value = value
self.next = next
class _LinkedList:
def __init__(self):
self.head = None
self.tail = None
self.n = 0
def append(self, key: int, value: int):
node = _ListNode(key, value)
if self.head is None:
self.head = node
self.tail = node
else:
self.tail.next = node
self.tail = self.tail.next
self.n += 1
def remove(self, key: int):
dummy = _ListNode()
dummy.next = self.head
predecessor, cursor = dummy, dummy.next
while cursor:
if cursor.key == key:
predecessor.next = cursor.next
self.n -= 1
self.head = dummy.next
return
predecessor, cursor = cursor, cursor.next
raise KeyError('Key does not exist!')
def __len__(self):
return self.n
class MyHashMap:
def __init__(self, cap=11, p=109345121):
"""创建一个空的映射"""
self._table = [_LinkedList() for _ in range(cap)]
self._n = 0
self._prime = p # MAD压缩函数中大于哈希表容量的大质数
self._scale = 1 + randrange(p - 1) # MAD压缩函数中的缩放系数a
self._shift = randrange(p) # MAD压缩函数中的偏移系数b
def _hash_function(self, key):
"""哈希函数"""
return (self._scale * hash(key) + self._shift) % self._prime % len(self._table)
def __len__(self):
return self._n
def get(self, key):
j = self._hash_function(key)
bucket = self._table[j]
cursor = bucket.head
while cursor:
if cursor.key == key:
return cursor.value
cursor = cursor.next
return -1
def put(self, key, value):
j = self._hash_function(key)
size = len(self._table[j])
bucket = self._table[j]
cursor = bucket.head
while cursor:
if cursor.key == key:
cursor.value = value
return
cursor = cursor.next
self._table[j].append(key, value) # 映射中不存在键key
if len(self._table[j]) > size: # k为新的键
self._n += 1
if self._n > len(self._table) // 2: # 确保负载系数小于0.5
self._resize(2 * len(self._table) - 1) # 通常2 * n - 1为质数
def remove(self, key):
j = self._hash_function(key)
bucket = self._table[j]
try:
bucket.remove(key)
self._n -= 1
return
except KeyError:
return
def _resize(self, cap):
"""将哈希表容量调整为cap"""
old = list(self) # 通过迭代获得已有的所有键值对
self._table = [_LinkedList() for _ in range(cap)]
self._n = 0
for key, value in old:
self.put(key, value)
def __iter__(self):
for bucket in self._table:
cursor = bucket.head
while cursor:
yield cursor.key, cursor.value
cursor = cursor.next
def main():
hash_map = MyHashMap()
# hash_map.put(1, 1)
# hash_map.put(2, 2)
# print(hash_map.get(1))
# print(hash_map.get(3))
# hash_map.put(2, 1)
# print(hash_map.get(2))
# hash_map.remove(2)
# print(hash_map.get(2))
if __name__ == '__main__':
main()
3. 解法二(线性查找法)
3.1 分析
详细分析请参考:【数据结构Python描述】仿照Python解释器使用哈希表手动实现一个字典。
3.2 解答
from random import randrange
class MyHashMap:
class _Item:
__slots__ = 'key', 'value'
def __init__(self, key, value):
self.key = key
self.value = value
_AVAIL = object() # 哨兵标识,用于标识被键值对被删除的哈希表单元
def __init__(self, cap=11, p=109345121):
"""创建一个空的映射"""
self._table = [None for _ in range(cap)]
self._n = 0
self._prime = p # MAD压缩函数中大于哈希表容量的大质数
self._scale = 1 + randrange(p - 1) # MAD压缩函数中的缩放系数a
self._shift = randrange(p) # MAD压缩函数中的偏移系数b
def _is_available(self, j):
"""当哈希表索引为j的单元处为空或键值对被删除,则返回True"""
return self._table[j] is None or self._table[j] is MyHashMap._AVAIL
def _find_slot(self, j, key):
"""查找索引为j的哈希表单元处是否有键k
该方法的返回值为一个元组,且返回的情况如下:
- 当在索引为j的哈希表单元处找到键k,则返回(True, first_avail);
- 当未在哈希表任何单元处找到键k,则返回(False, j)。
"""
first_avail = None
while True:
if self._is_available(j):
if first_avail is None:
first_avail = j
if self._table[j] is None:
return False, first_avail
elif key == self._table[j].key:
return True, j
j = (j + 1) % len(self._table)
def _hash_function(self, key):
"""哈希函数"""
return (self._scale * hash(key) + self._shift) % self._prime % len(self._table)
def __len__(self):
return self._n
def get(self, key):
j = self._hash_function(key)
found, s = self._find_slot(j, key)
if not found:
return -1
return self._table[s].value
def put(self, key, value):
j = self._hash_function(key)
found, s = self._find_slot(j, key)
if not found:
self._table[s] = self._Item(key, value)
self._n += 1
else:
self._table[s].value = value
if self._n > len(self._table) // 2: # 确保负载系数小于0.5
self._resize(2 * len(self._table) - 1) # 通常2 * n - 1为质数
def remove(self, key):
j = self._hash_function(key)
found, s = self._find_slot(j, key)
if not found:
return
self._table[s] = MyHashMap._AVAIL
self._n -= 1
def _resize(self, cap):
"""将哈希表容量调整为cap"""
old = list(self) # 通过迭代获得已有的所有键值对
self._table = [None for _ in range(cap)]
self._n = 0
for key, value in old:
self.put(key, value)
def __iter__(self):
for j in range(len(self._table)):
bucket = self._table[j]
if not self._is_available(j):
yield bucket.key, bucket.value
def main():
hash_map = MyHashMap()
hash_map.remove(14)
print(hash_map.get(4)) # -1
hash_map.put(7, 3)
hash_map.put(11, 1)
hash_map.put(12, 1)
print(hash_map.get(7)) # 3
hash_map.put(1, 19)
hash_map.put(0, 3)
hash_map.put(1, 8)
hash_map.put(2, 6)
if __name__ == '__main__':
main()
针对上述解答,可能有人对于方法 _find_slot()
有这样的疑问,即为什么当需要在未能于哈希表任何单元处找到键 key
时,该方法都是在 self._table[j]
为 None
的时候返回,是否有可能应该在 self._table[j]
为 MyHashMap._AVAIL
的时候返回呢?
针对上述问题,答案是否定的。对此,可以使用反证法来说明,假设在未能于哈希表任何单元处找到键 key
时,该方法于 self._table[j]
为 MyHashMap._AVAIL
的时候返回,这意味着此时哈希表的每个单元都应该引用 MyHashMap._AVAIL
。
接下来再来看上述情形什么时候会出现,实际上这只会在当哈希表的每个单元都先被引用了业务数据,之后又被执行了删除操作时发生;然而,上述给出的实现中,当哈希表超过一半的单元被引用业务数据之后,哈希表就会扩容了,所以不可能出现假设的情况,也即当在哈希表中找不到指定的键 key
时,方法 _find_slot()
一定是在 self._table[j]
为 None
的时候返回。