哈希表(Hash Table)
也称为散列表,是一种数据结构,它提供了快速的数据插入和查找功能。哈希表的工作原理基于哈希函数和数组,具体步骤如下:
-
哈希函数(Hash Function): 哈希函数是哈希表的核心,它接受一个键(Key)作为输入,并生成一个整数,这个整数通常称为哈希码(Hash Code)。哈希函数的设计需要满足以下条件:
- 确定性:相同的键总是产生相同的哈希码。
- 高效性:计算哈希码的速度要快。
- 均匀分布:不同的键应尽可能均匀地映射到哈希表的各个位置。
-
数组(Array): 哈希表底层通常使用数组来存储数据。数组的大小(也称为哈希表的大小或容量)通常是一个质数,以减少哈希冲突的可能性。
-
索引计算: 通过将哈希码对数组的大小进行取模运算,可以得到一个数组索引。这个索引决定了键值对在数组中的位置。
-
处理哈希冲突(Handle Collisions): 当两个不同的键通过哈希函数计算得到的哈希码相同,即发生了哈希冲突。处理哈希冲突的常见方法有:
- 链地址法(Chaining):在每个数组位置维护一个链表,所有映射到该位置的键值对都存储在这个链表中。
- 开放寻址法(Open Addressing):寻找空的数组位置来存储发生冲突的键值对,常见的策略有线性探测、二次探测和双重哈希。
-
动态扩容(Dynamic Resizing): 当哈希表的负载因子(已存储元素数量与数组大小的比值)超过某个阈值时,性能会下降。为了保持操作的效率,哈希表可能会进行扩容,即创建一个更大的数组,并将所有元素重新映射到新数组中。
-
时间复杂度: 理想情况下,哈希表的插入、删除和查找操作的时间复杂度为O(1)。但在最坏情况下,如果所有元素都映射到同一个位置,时间复杂度会退化到O(n),其中n是元素的数量。
哈希表在实际应用中非常广泛,例如数据库索引、缓存实现、快速查找等场景。设计一个好的哈希函数和选择合适的冲突解决策略对于哈希表的性能至关重要。
在哈希表中,存储的是键值对的指针(或引用)。在计算机科学中,"指针"通常指的是内存地址,它指向存储数据的确切位置。在高级编程语言中,如Python,我们通常使用引用来间接访问对象,这在概念上类似于指针。
每个哈希表的"桶"(bucket)实际上是一个链表的头节点,每个节点(HashNode)包含了键(key)、值(value)和指向下一个节点的指针(next)。这样设计允许多个键值对共享同一个哈希索引,通过链表的形式解决哈希冲突。
这里是关键点的解释:
- 键值对:在哈希表中,每个元素是一个键值对,键是用于查找元素的标识符,值是与键相关联的数据。
- 指针/引用:在内存中,键值对实际上是作为对象存储的。在哈希表的数组中,我们不直接存储键值对对象本身,而是存储指向这些对象的指针或引用。
当我们在哈希表中插入一个键值对时,实际上是创建了一个包含键和值的HashNode对象,并将其地址(或引用)存储在哈希表的数组中。当我们查询或删除一个键值对时,我们通过键来找到对应的HashNode对象,并操作这个对象的值或从链表中移除这个节点。
在不同的编程语言中,这种指针或引用的实现细节可能有所不同,但基本概念是一致的。在像C或C++这样的语言中,你可能会直接使用指针来操作内存;而在像Python这样的高级语言中,引用是通过对象的内存地址来间接实现的,但程序员通常不需要直接操作这些内存地址。
HashMap 和 HashSet 是 Java 集合框架中的一部分,它们都基于哈希表的原理实现,但用途和特性有所不同:
-
HashMap:
HashMap是一种基于哈希表的Key-Value映射。它存储了键值对(key-value pairs),其中每个键映射到一个特定的值。- 键必须提供合适的
hashCode()实现,以确保键能够在哈希表中均匀分布。 HashMap不保证映射的顺序,这意味着遍历HashMap时元素的顺序可能会在不同的时间点发生变化。HashMap允许一个键对应一个值,并且允许键为null,但最多只能有一个null键。值也可以为null。
-
HashSet:
HashSet是基于HashMap实现的,它用来存储一组不允许重复的元素。HashSet实际上是使用元素本身作为键,而将元素的哈希值作为值存储在内部的HashMap中。- 由于
HashSet是基于HashMap实现的,所以它也不允许null值,但可以有一个null元素。 HashSet不保证元素的顺序,并且每次添加元素时都会检查是否已存在具有相同哈希码的元素,以避免重复。
以下是一些HashMap和HashSet的常用操作:
-
HashMap:
put(K key, V value):将指定的值与此映射中的指定键关联。get(Object key):返回指定键所映射的值。remove(Object key):如果存在一个键的映射关系,则将其从映射中移除。keySet():返回映射中包含的键的Set视图。
-
HashSet:
add(E e):将指定元素添加到此集合中。remove(Object o):如果存在元素对象,则从集合中移除。contains(Object o):如果集合包含指定的元素,则返回true。
HashMap 和 HashSet 都是线程不安全的,如果需要线程安全,可以使用 Collections.synchronizedMap(new HashMap<...>()) 或 Collections.synchronizedSet(new HashSet<...>()) 来包装它们,或者使用 ConcurrentHashMap。
Java 8 以后,HashMap 和 HashSet 还引入了一些新的功能,如HashMap的getOrDefault(Object key, V defaultValue)方法,以及HashSet的removeIf(Predicate<? super E> filter)方法等。
HashMap 内部实现机制较为复杂,下面是对其内部实现的详细介绍:
1. **数据结构**:HashMap 是基于哈希表的 Map 接口实现,以键值对(key-value)的形式存储数据。在 JDK 1.8 之前,HashMap 由数组+链表组成,而在 JDK 1.8 之后,当链表长度大于阈值(默认为 8)且数组长度大于 64 时,链表会转换为红黑树,以提高查询效率。
2. **哈希函数**:HashMap 使用哈希函数将键映射到数组的索引位置。通过取键的 hashCode 值、进行高位运算和取模运算等步骤,得到最终的索引值。这样可以在数组中快速定位键值对的存储位置。
3. **存储过程**:当向 HashMap 中添加键值对时,首先通过哈希函数计算键的索引值。如果该索引位置没有数据,则直接将键值对存储到数组中;如果该索引位置已经存在数据,则会发生哈希冲突。在哈希冲突的情况下,HashMap 会根据具体情况选择不同的解决方式。如果键的内容相等,则直接覆盖旧值;如果键的内容不相等,则将新的键值对添加到链表的头部或红黑树中。
4. **扩容机制**:为了避免哈希冲突过多导致性能下降,HashMap 会在元素个数超过数组大小乘以负载因子(默认值为 0.75)时进行扩容。扩容时,会重新计算每个元素的哈希值,并将其重新分配到新的数组位置上。
5. **特点**:HashMap 具有存取无序、键和值都可以为 null、键唯一等特点。它在查找、插入和删除操作上都具有较高的性能,但不保证线程安全。在多线程环境下,需要使用线程安全的 ConcurrentHashMap 或进行适当的同步处理。
总的来说,HashMap 通过哈希函数和数据结构的优化,实现了高效的键值对存储和查询。但在使用时需要注意线程安全问题,并根据实际需求合理调整负载因子和初始容量等参数,以获得最佳性能。如果你需要更详细和准确的信息,建议参考 Java 官方文档或相关的专业书籍。
HashSet 是基于哈希表实现的,其内部使用 HashMap 来存储元素。
HashSet 内部实现的详细机制:
1. **哈希表数据结构**:HashSet 内部使用一个哈希表来存储元素。哈希表是一个数组,每个元素被存储在数组的一个特定位置,这个位置由元素的哈希码(hash code)确定。哈希码是通过元素的 hashCode()方法计算得到的。
2. **添加元素**:当向 HashSet 中添加一个元素时,HashSet 首先计算该元素的哈希码。然后,它使用哈希码来确定在哈希表中的存储位置。如果该位置为空,那么元素将被直接存储在这个位置。如果该位置不为空(即发生了哈希冲突),则 HashSet 会使用链表或更高效的数据结构(如红黑树,在 Java 8 及更高版本中引入)来存储具有相同哈希码的元素。
3. **确保唯一性**:HashSet 确保其中不会有重复元素。它通过比较元素的哈希码和 equals()方法来检查元素的唯一性。如果两个元素的哈希码相同,HashSet 会调用它们的 equals()方法来进一步比较它们是否相等。如果 equals()返回 true,HashSet 将不会存储第二个相同的元素。
4. **查询元素**:当查询 HashSet 中是否包含某个元素时,HashSet 会计算该元素的哈希码,并根据哈希码来查找存储位置。然后,它会使用 equals()方法来检查是否存在相同的元素。
5. **删除元素**:当尝试从 HashSet 中删除一个元素时,HashSet 会计算该元素的哈希码,然后查找存储位置。如果找到元素,它将被删除。如果存在哈希冲突,HashSet 会在链表或红黑树中查找并删除相应的元素。
6. **扩容机制**:HashSet 的底层是 HashMap,当元素个数超过数组大小乘以负载因子(默认值为 0.75)时,HashSet 会进行扩容。扩容时,会重新计算每个元素的哈希值,并将其重新分配到新的数组位置上。
需要注意的是,HashSet 不保证元素的顺序,元素的存储顺序与它们的哈希码有关。如果需要有序的集合,可以考虑使用 LinkedHashSet,它会维护元素的插入顺序,或者使用 TreeSet,它会按照元素的自然顺序或自定义比较器来进行排序。

1998

被折叠的 条评论
为什么被折叠?



