常用数据结构-哈希表 (Hash Table)


🔍 哈希表 (Hash Table)

哈希表是一种使用哈希函数将键映射到存储位置的数据结构,以实现快速数据查找。

1️⃣ 特点

  • 快速的查找、插入和删除 由于直接访问内存位置,常数时间复杂度内可以完成操作。
  • 键值对存储 存储数据的形式为键值对。
  • 冲突处理 当两个键的哈希值相同时,需要一种机制来处理这种冲突,常见的方法有链地址法和开放地址法等。
  • 动态扩容 为了保持操作的效率,当哈希表的装载因子超过一定阈值时,通常需要进行扩容。

2️⃣ Java中有哪些常用哈希表,属于那种哈希表


  • HashMap

    • 描述 HashMap 是 Java 中最常用的哈希表实现。
    • 特点
      • 允许 null 键和 null 值。
      • 内部实现为数组+链表/红黑树结构。
      • 不保证映射的顺序,特别是不保证该顺序恒久不变。
    • 示例
      HashMap<String, Integer> hashMap = new HashMap<>();
      hashMap.put("one", 1);
      Integer value = hashMap.get("one");
      
  • Hashtable

    • 描述 Hashtable 是一个古老的哈希表实现。
    • 特点
      • 不允许 null 键和 null 值。
      • 线程安全,所有的公有方法都使用了synchronized修饰。
      • 性能上较 HashMap 略慢,因为它同步所有操作。
    • 示例
      Hashtable<String, Integer> hashtable = new Hashtable<>();
      hashtable.put("one", 1);
      
  • LinkedHashMap

    • 描述 LinkedHashMapHashMap 的一个子类,维护了访问的顺序。
    • 特点
      • 可以按照插入顺序或访问顺序排序。
      • 通常用于实现LRU缓存策略。
    • 示例
      LinkedHashMap<String, Integer> lhm = new LinkedHashMap<>(16, 0.75f, true);
      lhm.put("three", 3);
      lhm.get("three");
      
  • ConcurrentHashMap

    • 描述 ConcurrentHashMap 是一个线程安全的哈希表实现。
    • 特点
      • 采用分段锁技术,允许多个修改操作并发进行。
      • 在 Java 8 中,分段锁技术已被废弃,转而使用了红黑树。
      • 支持高并发的读写操作。
    • 示例
      ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
      chm.put("four", 4);
      

3️⃣ Java中哈希表的使用

  • 初始化
    HashMap<String, Integer> map = new HashMap<>();
    
  • 插入
    map.put("key", 1);
    
  • 查找
    int value = map.get("key");
    
  • 删除
    map.remove("key");
    

4️⃣ Java中的常用场景

  • 数据去重 使用HashSet或HashMap来快速去除重复数据。
  • 缓存实现 使用HashMap或LinkedHashMap来实现简单的缓存。
  • 快速查找 在需要快速查找数据是否存在的场景中使用。

5️⃣ 工具类的使用

  • Apache Commons Collections 提供了一些增强的哈希表实现和相关的工具类。
  • Google Guava 提供了多种强大的哈希表实现,如MultimapBiMap等。

⚠️ 注意: 在使用哈希表时,应考虑哈希函数的选择、装载因子和扩容策略,以确保性能。


📌 哈希取模

哈希取模是在哈希表中分配键值对的常用方法。

  • 过程:

    1. 首先,使用哈希函数计算给定键的哈希值。
    2. 使用取模操作确定该键值对应该存储在哈希表的哪个位置。这通常是通过将哈希值模上哈希表的大小来实现的。
  • 用途:

    • 通过取模操作,可以确保分配的哈希位置在哈希表的大小范围内。
    • 这种方法也有助于在哈希表中均匀地分配键值对,减少碰撞。
  • 示例:

    int hash = key.hashCode();
    int index = hash % tableSize;
    

📌 数学归纳法

数学归纳法是一种数学证明方法,用于证明一系列语句中的每一条都是真的。

  • 过程:

    1. 基础步骤 (Base Step): 首先证明该语句对于最小的值(通常是1或0)是成立的。
    2. 归纳假设 (Inductive Hypothesis): 假设该语句对于某个特定的值k是成立的。
    3. 归纳步骤 (Inductive Step): 在归纳假设的基础上证明该语句对于k+1也是成立的。
  • 用途:

    • 数学归纳法经常用于证明序列或数列中的性质或属性。
  • 示例:
    假设我们想要证明对于所有非负整数n,有 (1 + 2 + 3 + … + n = \frac{n(n + 1)}{2}) 成立:

    1. 基础步骤: 当n=1时,左边的和为1,右边为(\frac{1(1+1)}{2})。两边相等。
    2. 归纳假设: 假设该公式对于某个特定的值k成立。
    3. 归纳步骤: 基于上述假设,证明该公式对于k+1也成立。

经过这三个步骤,我们可以得出结论:该公式对于所有非负整数n都成立。


📌 哈希码与哈希冲突

  • 📜 哈希码的定义

    • 在Java中,每个对象都有一个与之关联的哈希码,可以通过对象的 hashCode() 方法获取。
    • 哈希码通常用作快速访问数据结构(如哈希表)中的对象的方法,因为哈希码的计算通常比完整的对象比较要快。
  • 📎 哈希冲突的原因

    • 哈希函数是将大范围的输入映射到较小范围的输出的函数。
    • 由于输出范围的大小是有限的,而输入范围可能是无限的或非常大的,因此不同的输入可能会被映射到同一个输出,导致哈希冲突。
  • 📎 哈希函数的设计与冲突

    • 哈希函数的目标是均匀地分布输出,以减少冲突。
    • 但实际的函数可能并不完美,有些函数可能更容易导致冲突。
    • 在Java中,对象的 hashCode() 方法提供了对象的哈希值,但很多类都重写了这个方法以提供更有意义的哈希值。

📌 对象的唯一性

  • 📜 在Java中,对象的唯一性通常由两个因素共同确定:

    1. 哈希码(通过 hashCode() 方法得到)
    2. 相等性(通过 equals() 方法确定)
  • 📎 为什么需要哈希码和相等性

    • 哈希码可能会冲突,因此我们不能仅仅依赖哈希码来确定对象的唯一性。
    • 当我们在哈希表中查找一个键时,首先会使用其哈希码来定位可能的位置。找到具有相同哈希码的键后,会使用 equals() 方法进一步确认这是我们要找的确切键。
  • 📌 hashCode()equals() 关系:

    • 📎 当两个对象按照 equals() 方法比较是相等的,那么它们的 hashCode() 必须相同。
    • 📎 两个对象的 hashCode() 相同,并不意味着它们一定相等。
  • 📝 为什么会这样?:

    • 📍 hashCode() 方法返回的是一个int值,这意味着只有 (2^{32}) 个可能的哈希码值。
    • 📍 可能存在的对象数量是无限的,因此许多不同的对象可能会有相同的哈希码。

⚠️ 注意: 当重写 equals() 方法时,为了维护这种关系,通常也需要重写 hashCode() 方法。


📌 常见的冲突处理方法

  • 📎 链地址法 (Separate Chaining)

    • 每个表项不再是单一记录,而是一个链表。
    • 当新的记录的哈希值与已存在的记录的哈希值冲突时,将其添加到相应的链表中。
    • 需要额外的内存来存储链表结构。
    public class HashTable<K, V> {
        private LinkedList<Entry<K, V>>[] table;
    
        public void put(K key, V value) {
            int index = hash(key);
            if (table[index] == null) {
                table[index] = new LinkedList<>();
            }
            table[index].add(new Entry<>(key, value));
        }
    }
    

⚠️ 注意: 如果链表过长,查找效率可能会降低。为了保持效率,当链表长度超过某个阈值时,可能需要调整哈希表的大小。


  • 📎 开放地址法 (Open Addressing)

    • 当发生冲突时,寻找下一个可用的位置。
    • 常见的探查策略有线性探查、二次探查和双重哈希等。
    • 所有的记录都存储在哈希表本身,无需额外的数据结构。
    public class HashTable<K, V> {
        private Entry<K, V>[] table;
    
        public void put(K key, V value) {
            int index = hash(key);
            while (table[index] != null) {
                index = (index + 1) % table.length;  // 线性探查
            }
            table[index] = new Entry<>(key, value);
        }
    }
    

⚠️ 注意: 为了保持查找效率,通常当哈希表被填满到一定程度时,会进行扩容。


📌 LRU (Least Recently Used) 缓存策略

LRU(Least Recently Used,最近最少使用)缓存是一种常用的缓存策略。它根据数据项的访问顺序来确定哪些数据项是最近最少使用的,从而进行缓存的替换。


📍 实现步骤

1️⃣ 定义缓存的容量

  • 确定缓存可以存储的数据项数量上限。
  • 当缓存达到容量上限时,需要进行替换。

2️⃣ 使用数据结构支持快速访问和删除操作

  • 使用双向链表和哈希表的组合。
  • 双向链表用于保持数据项的访问顺序。
  • 哈希表用于实现快速的数据项查找。

3️⃣ 数据项访问时的操作

  • 检查数据项是否在缓存中存在。
    • 如果存在,将数据项移到链表的头部。
    • 如果不存在,将其添加到缓存中。如果缓存已满,删除链表尾部的数据项并添加新数据项到链表头部。

4️⃣ 数据项删除操作

  • 当缓存达到容量上限时,删除链表尾部的数据项。
  • 在哈希表中快速找到要删除的数据项,并更新链表的前后指针。

⚠️ 注意 在实际编码中,可以使用Java提供的数据结构如 LinkedHashMapLinkedHashSet。这些数据结构已经提供了LRU缓存的特性,简化代码实现。在多线程环境下,缓存的并发访问需要同步操作,保证线程安全。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yueerba126

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值