拉链法
拉链法是一种常用的解决哈希冲突的方法,它通过在哈希表的每个槽(位桶)中使用链表(在Java 8中,为了提高性能,链表在长度超过一定阈值时会转换成红黑树)来存储具有相同哈希值的元素。这种方法的特点是简单且易于实现,下面是对拉链法及其操作的润色描述:
- 插入操作:当发生哈希冲突时,即两个或多个关键字被映射到同一个槽时,新元素会被添加到该槽的链表中。插入操作的时间复杂度在最坏情况下为O(1),因为每个槽的元素数量不会超过装载因子(n/m,其中n是元素数量,m是槽的数量)。
- 查询操作:查询操作与插入类似,需要遍历发生冲突的槽中的链表。在最坏情况下,查询的时间复杂度也是O(1)。
- 删除操作:在拉链法中,如果使用链表作为位桶的实现,那么链表应该是双向的,以便在删除元素时能够正确地更新前驱节点的指针。删除操作的时间复杂度同样为O(1)。
拉链法的优点包括:
- 处理冲突简单,没有堆积现象,即不同关键字不会发生冲突。
- 适合于在创建哈希表前无法确定表长的情况,因为链表的节点空间是动态分配的。
- 可以设置较高的装载因子,节省空间。
- 删除操作容易实现。
拉链法的缺点是: - 需要额外的空间来存储指针,这在元素较小时可能导致空间浪费。
以下是拉链法(Chaining)的示意图。这个图展示了一个简单的哈希表,其中包含三个槽(buckets),每个槽通过链表来解决哈希冲突。
在这个图中:
- 哈希表(HT) 是整个数据结构的主体,它包含多个槽(buckets)。
- 槽(B1, B2, B3) 是哈希表中的存储位置,每个槽可以存储多个元素。
- 元素(E1, E2, E3, E4, E5, E6) 是存储在哈希表中的数据项。
- 链表 用于在每个槽中解决哈希冲突,当两个元素映射到同一个槽时,它们会被存储在同一个链表中。
这种结构使得即使多个元素映射到同一个槽,它们也可以被有效地存储和检索。
开放地址法
开放地址法是另一种解决哈希冲突的方法,它要求所有元素都存储在哈希表中,即装载因子不超过1。开放地址法通过使用探查序列来寻找空槽位,以下是几种常用的探查序列:
- 线性探查:当发生冲突时,按照线性序列(如1, 2, 3, …, m-1)顺序查找下一个空槽位。
- 二次探查:使用二次序列(如1^2, -1^2, 2^2, -2^2, …, k^2, -k^2,其中k<=m/2)来查找空槽位,这种方法在表中进行跳跃式探测。
- 伪随机探测:使用伪随机数序列来查找空槽位,通常需要一个伪随机数生成器来生成序列。
开放地址法的缺点包括:
- 每次冲突都需要重新计算哈希值,增加了计算时间。
- 删除操作复杂,因为需要标记已删除的元素以避免在探查时误认为是空槽。
以下是使用Mermaid语法描述的开放地址法(Open Addressing)的示意图。这个图展示了一个简单的哈希表,其中包含多个槽(buckets),每个槽直接存储元素,当发生哈希冲突时,会使用探查序列在表中寻找下一个空闲的槽。
在这个图中:
- 哈希表(HT) 是整个数据结构的主体,它包含多个槽(buckets)。
- 槽(B1, B2, …, B9) 是哈希表中的存储位置,每个槽可以存储一个元素。
- 元素(如元素1, 元素2, …, 元素6) 是存储在哈希表中的数据项。
- 空闲 表示该槽没有存储任何元素,是空的。
开放地址法处理冲突的方式是在发生冲突时,使用探查序列(如线性探查、二次探查或伪随机探测)在表中寻找下一个空闲的槽。例如:
- 线性探查 会顺序检查下一个槽(B1 -> B2 -> B3 -> …)直到找到一个空闲槽。
- 二次探查 会按照二次序列跳跃检查槽位(B1 -> B4 -> B7 -> …)。
- 伪随机探测 会使用伪随机数序列来选择下一个槽。
这种结构使得所有元素都存储在哈希表内部,没有使用额外的链表或数据结构。开放地址法的优点是空间利用率高,但缺点是删除操作复杂,需要特别标记已删除的元素,以避免在探查时误认为是空槽。
再散列法
再散列法是一种简单的解决哈希冲突的方法,它通过重新应用哈希函数来寻找新的槽位,直到找到一个没有冲突的位置。这种方法的缺点是每次冲突都需要重新计算哈希值,增加了计算成本。
在实际应用中,HashMap和HashSet等Java集合类就是使用拉链法来解决哈希冲突的。而ThreadLocal中的ThreadLocalMap则使用了线性探查的开放地址法。这些方法的选择取决于具体的应用场景和性能要求。