https://leetcode-cn.com/problems/design-hashset/solution/she-ji-ha-xi-ji-he-by-leetcode/
概述
LC705这是教科书上一个经典问题,用来测试一个人的数据结构知识。因此,是不可以用任何内置的 HashSet 数据结构来解决此问题。
为了实现 HashSet 数据结构,有两个关键的问题,即哈希函数和冲突处理。
- 哈希函数:目的是分配一个地址存储值。理想情况下,每个值都应该有一个对应唯一的散列值。
- 冲突处理:哈希函数的本质就是从 A 映射到 B。但是多个 A 值可能映射到相同的 B。这就是碰撞。因此,我们需要有对应的策略来解决碰撞。总的来说,有以下几种策略解决冲突:
- 单独链接法:对于相同的散列值,我们将它们放到一个桶中,每个桶是相互独立的。
- 开放地址法:每当有碰撞, 则根据我们探查的策略找到一个空的槽为止。
- 双散列法:使用两个哈希函数计算散列值,选择碰撞更少的地址。
在本文中,我们使用单独链接法,来看看它是如何工作的。
从本质上讲,HashSet 的存储空间相当于连续内存数组。这个数组中的每个元素相当于一个桶。
给定一个值,我们首先通过哈希函数生成对应的散列值来定位桶的位置。
一旦找到桶的位置,则在该桶上做相对应的操作,如 add,remove,contains。
方法一:单独链表法
哈希函数的共同特点是使用模运算符。hash=value mod base
。其中,base
将决定 HashSet
中的桶数。
从理论上讲,桶越多(因此空间会越大)越不太可能发生碰撞。base
的选择是空间和碰撞之间的权衡。
此外,使用质数作为 base
是一个明智的选择。例如 769
,可以减少潜在的碰撞。
对于桶的设计,我们有几种选择,可以使用数组来存储桶的所有值。然而数组的一个缺点是需要 O(N)
的时间复杂度进行插入和删除,而不是 O(1)
。
因为任何的更新操作,我们首先是需要扫描整个桶为了避免重复。选择链表来存储桶的所有值是更好的选择,插入和删除具有常数的时间复杂度。
1.1单独链接法算法:
正如我们在上面讨论的,这里将采用 LinkedList 实现 HashSet 中的桶。
对于每个功能 add,remove,contains,我们首先生成桶的散列值,操作相对应的桶。
class MyHashSet {
private Bucket[] bucketArray;
private int keyRange;
/** Initialize your data structure here. */
public MyHashSet() {
this.keyRange = 769;
this.bucketArray = new Bucket[this.keyRange];
for (int i = 0; i < this.keyRange; ++i)
this.bucketArray[i] = new Bucket();
}
protected int _hash(int key) {
return (key % this.keyRange);
}
public void add(int key) {
int bucketIndex = this._hash(key);
this.bucketArray[bucketIndex].insert(key);
}
public void remove(int key) {
int bucketIndex = this._hash(key);
this.bucketArray[bucketIndex].delete(key);
}
/** Returns true if this set contains