JAVA实现哈希表:MyHashMap

实现一个哈希表(HashMap),以下是对代码的详细解释:

  • TNode 类:

    • TNode 类表示哈希表中的节点,包含键(key)、值(value)、下一个节点的引用(next)以及键的哈希值(hash)。
    • 构造方法 TNode(K key, V value, int hash) 用于初始化节点。
class TNode {
    K key;
    V value;
    TNode next;
    int hash;// K的hash值
    public TNode(K key, V value, int hash) {
        this.key = key;
        this.value = value;
        this.hash = hash;
    }
}
  • MyHashMap 类:

    • MyHashMap 类表示哈希表,使用泛型来支持键值对的存储。
    • 成员变量包括 table(哈希表数组)、modCount(数组被占用的格子数量)、elmSize(元素个数)、capacity(数组的容量)。
    • 定义了默认数组容量 DEFAULT_CAPACITY 和负载因子 LOAD_FACTOR
public class MyHashMap {
    private TNode[] table;
    private int modCount;// 数组被占用的格子数量
    private int elmSize;// 元素个数
    private int capacity;// 数组的容量
    private static final int DEFAULT_CAPACITY = 16;
    private static final double LOAD_FACTOR = 0.75;
}
  • 构造方法:

    • MyHashMap(int initCapacity): 构造方法用于初始化哈希表数组,根据传入的初始容量设置数组大小。
public MyHashMap(int initCapacity) {
        if (initCapacity < DEFAULT_CAPACITY) {
            initCapacity = DEFAULT_CAPACITY;
        }
        table = new TNode[initCapacity];
        capacity = initCapacity;
        elmSize = 0;
        modCount = 0;
    }
  • put 方法:

    • put(K key, V value): 用于向哈希表中存储键值对。计算键的哈希值,创建新节点,并根据哈希值和数组长度确定存储位置。处理冲突,若位置为空则直接存储新节点,否则在链表末尾添加新节点。如果数组被占用格子数量超过负载因子,进行扩容。
public void put(K key, V value) {
        //System.out.println("Put");
        int h = key.hashCode();// 计算Key 的hash
        TNode elmNode = new TNode<>(key, value, h);// 新节点
        int index = h & (capacity - 1);// 根据hash值 与 数组长度 得到下标
        // System.out.println(index);
        TNode first = table[index];
        if (first == null) {
            table[index] = elmNode;
            modCount++;
            elmSize++;
        } else {
            TNode oldNode = null;
            // 如果first 与 新节点的key一致 更新 first节点的v
            if (first.hash == h && first.key == key || first.key.equals(key)) {
                first.value = value;
                oldNode = new TNode<>(key, first.value, h);
            } else {
                TNode temp = first;
                while (temp.next != null) {
                    temp = temp.next;
                    if (temp.hash == h && temp.key == key || temp.key.equals(key)) {
                        temp.value = value;
                        oldNode = new TNode<>(key, temp.value, h);
                        break;
                    }
                }
                if (oldNode == null) {// 新增节点
                    temp.next = elmNode;
                    elmSize++;
                }
            }
        }


        // 扩容: 数组的被占用格子数 比例大于数组容量的百分之75的时候扩容
        if (modCount >= capacity * LOAD_FACTOR) {
            resize();
        }
    }
  • resize 方法:

    • resize(): 扩容方法,将数组大小加倍,重新哈希并存储旧数组中的所有元素。
// 扩容:
    // 扩容 2倍扩容
    // 创建一个更大的数组 将原本的所有元素存储进去 (每个元素取出 重新映射位置 )
    // 每个元素存储的位置与当前数组的长度相关
    private void resize() {
        //System.out.println("扩容进入:" + capacity);
        modCount = 0;
        elmSize = 0;
        int oldCapacity = capacity;
        int newCapacity = oldCapacity + oldCapacity;
        TNode[] oldTable = table;
        TNode[] newTable = new TNode[newCapacity];
        for (int i = 0; i < oldCapacity; i++) {
            //System.out.println("扩容循环");
            // 取出旧节点
            TNode node = oldTable[i];
            while (node != null) {
                TNode newNode = new TNode<>(node.key, node.value, node.hash);
                //System.out.println("遍历旧数组中的链表");
                int h = node.hash;
                int index = h & (newCapacity - 1);
                TNode first = newTable[index];
                if (first == null) {
                    newTable[index] = newNode;
                    elmSize++;
                    modCount++;
                } else {
                    //System.out.println("新数组目标位置是一条链表");
                    TNode temp = first;
                    while (temp.next != null) {
                    // System.out.println("temp.next !=null");
                    // System.out.println(temp.value);
                        temp = temp.next;
                    }
                    temp.next = newNode;
                    elmSize++;
                }
                node = node.next;
            }
        }
        table = newTable;
        capacity = newCapacity;
        System.out.println("扩容:" + capacity);
    }
  • get 方法:

    • get(K key): 用于从哈希表中获取键对应的值。计算键的哈希值,根据哈希值和数组长度确定位置,然后在链表中查找对应的节点。
public V get(K key) {
        int h = key.hashCode();
        int index = h & (capacity - 1);
        TNode first = table[index];
        // 判断 目标位置的节点是否为null
        if (first != null) {
        // 判断 first 是否是咱们需要的节点
        // 1: 哈希值一致 才需要比较后面的
        // 2: 接着比较key的引用地址 如果地址一致 就不需要比较内容
        // 3: 地址不一致的情况会存在内容一致的情况 使用equals比较
            if (first.hash == h && first.key == key || first.key.equals(key)) {
                return first.value;
            }
            // 第一个节点比较不成功 从这个节点开始作为头节点遍历链表
            TNode temp = first;// 底层: 判断 first 是链表节点还是红黑树节点
            while (temp.next != null) {
                temp = temp.next;
                if (temp.hash == h && temp.key == key || temp.key.equals(key)) {
                    return (V) temp.value;
                }
            }
        }
        return null;
    }
  • main 方法:

    • 在主方法中演示了如何创建一个 MyHashMap 实例,存储键值对,并输出数组长度、数组被占用格子数量和元素个数。
public static void main(String[] args) {
        MyHashMap map = new MyHashMap<>(16);
        for (int i = 0; i < 10000; i++) {
            map.put("Hello" + i, i);
        }
        System.out.println("数组长度:" + map.capacity);
        System.out.println("数组被占用格子数量:" + map.modCount);
        System.out.println("元素个数:" + map.elmSize);

        MyHashMap map2 = new MyHashMap<>(16);
        for (int i = 0; i < 100; i++) {
            map2.put("Hello" + i, i);
        }
        System.out.println("数组长度:" + map2.capacity);
        System.out.println("数组被占用格子数量:" + map2.modCount);
        System.out.println("元素个数:" + map2.elmSize);
    }

这段代码实现了基本的哈希表功能,包括键值对的存储、冲突处理、扩容等。通过哈希函数将键映射到数组位置,并在需要时进行扩容,确保了哈希表的高效性。

哈希表是一种常见的数据结构,它通过将关键字映射到表中的位置来实现快速的数据检索

  • 哈希函数

    哈希表的核心是哈希函数,它将关键字映射到表中的位置。好的哈希函数应该具有均匀分布的特性,尽量避免冲突。

  • 哈希冲突

    哈希冲突指两个不同的关键字被映射到了相同的位置。解决冲突的方法包括链地址法、开放地址法等。

  • 链地址法

    使用数组和链表结合的方式,每个数组位置上存储一个链表。哈希冲突时,新的元素加入到对应位置的链表中。
  • 在该方法中,哈希表的每个槽位都维护一个链表。当冲突发生时,新的键值对被添加到相应槽位的链表中。这样,具有相同哈希值的键值对都存储在同一个链表上。

  • 优点:简单易实现,适用于任何类型的对象。

  • 缺点:需要额外的空间来存储链表,可能导致内存浪费。

  • 开放地址法

    当发生冲突时,通过一定规则寻找下一个可用的位置,直到找到为止。

线性探测(Linear Probing)

线性探测是一种开放地址法的冲突解决方法。当哈希表中的某个槽位已被占用时,线性探测会在哈希表中顺序查找下一个可用的槽位,直到找到一个空槽位为止。

详细步骤:

  1. 计算键的哈希值,确定其在哈希表中的初始位置。
  2. 如果初始位置为空,则将键值对存储在该位置。
  3. 如果初始位置被占用,沿着哈希表的下一个位置顺序查找,直到找到一个空槽位。
  4. 将键值对存储在找到的空槽位上。

优点:实现简单,易于理解和实现。

缺点:可能导致聚集,即连续的槽位被占用,影响性能。查找速度可能变慢,因为需要线性搜索。

二次探测(Quadratic Probing)

二次探测是另一种开放地址法的冲突解决方法。与线性探测不同,二次探测使用二次方程来计算下一个探测位置,从而减少线性探测可能导致的聚集问题。

详细步骤:

  1. 计算键的哈希值,确定其在哈希表中的初始位置。
  2. 如果初始位置为空,则将键值对存储在该位置。
  3. 如果初始位置被占用,使用二次探测公式计算下一个位置,直到找到一个空槽位。
  4. 将键值对存储在找到的空槽位上。

优点:缓解了线性探测可能导致的聚集问题。

缺点:仍然可能存在聚集,性能可能不如其他更复杂的方法。

双重哈希(Double Hashing)

双重哈希是一种使用两个不同的哈希函数来解决冲突的方法。当发生冲突时,通过第二个哈希函数计算新的槽位。

详细步骤:

  1. 计算键的第一个哈希值,确定其在哈希表中的初始位置。
  2. 如果初始位置为空,则将键值对存储在该位置。
  3. 如果初始位置被占用,使用第二个哈希函数计算下一个位置,直到找到一个空槽位。
  4. 将键值对存储在找到的空槽位上。

优点:减少了聚集的可能性,提高了性能。

缺点:实现相对复杂,需要设计两个哈希函数。需要谨慎选择两个哈希函数,以避免产生相同的探测序列。

  • 装载因子

    装载因子是哈希表中已存储元素个数与数组长度的比值。当装载因子超过一定阈值,通常为0.75,就需要进行扩容操作,以维持较低的冲突率。

  • 扩容

    当哈希表需要扩容时,通常会创建一个新的更大的数组,然后重新映射原有的元素到新数组中。这涉及到重新计算哈希函数和解决新的哈希冲突。

  • hashCode和equals方法

    在Java中,要使用自定义对象作为哈希表的键,需要正确实现hashCodeequals方法。hashCode用于计算哈希值,而equals用于判断两个对象是否相等。

  • 实现哈希表的自定义类

    在实现自定义的哈希表时,需要考虑合适的哈希函数、解决冲突的方法,以及正确处理扩容等操作。

你可能感兴趣的:(哈希算法,散列表,算法)