【轻松拿捏】HashMap-详解及底层实现原理?

目录

1. 基本结构

2. 哈希函数

3. 哈希冲突解决

4. 插入操作(put)

5. 查找操作(get)

6. 删除操作(remove)

7. 扩容(resize)

8.说一下 HashMap 的实现原理?(面试)

8.1. 哈希函数

8.2. 数组

8.3. 冲突处理

8.4. 负载因子和扩容

8.5. 操作时间复杂度

8.6. Java 中的 HashMap 细节

9.总结


🎈边走、边悟🎈迟早会好

        HashMap  是一种非常常用的数据结构,特别适合需要高效查找、插入和删除操作的场景。下面将对 HashMap的内部机制进行详细讲解。

1. 基本结构

        HashMap是基于哈希表实现的。在 Java 中,它由一个数组和链表(在 Java 8 之前)或红黑树(在 Java 8 及之后)组成。

  • 数组:存储键值对的基础结构。
  • 链表:用于解决哈希冲突的链地址法的一种实现。
  • 红黑树:当链表长度超过一定阈值时,链表会转换为红黑树,以优化性能。

2. 哈希函数

哈希函数用于将键映射到数组中的一个索引位置。Java 中,HashMap的哈希函数通过以下步骤生成:

  1. 计算键的哈希值:使用 hashCode() 方法计算键的哈希值。
  2. 扰动函数:通过高位参与运算,减少冲突。Java 使用的扰动函数如下:
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

3. 哈希冲突解决

  • 链地址法(Chaining):在一个桶中存储一个链表,当多个键映射到同一个桶时,这些键值对存储在链表中。
  • 红黑树:当一个桶中的链表长度超过 8 时,链表转换为红黑树,提高查找、插入和删除操作的效率。

4. 插入操作(put)

        插入一个键值对时,首先计算键的哈希值,然后确定数组索引。如果该索引处没有元素,则直接插入。如果该索引处有元素(哈希冲突),则检查链表或红黑树中是否已经存在该键。如果存在则更新值,否则将新键值对插入到链表或红黑树中。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

5. 查找操作(get)

        查找键对应的值时,计算键的哈希值,然后定位到数组的索引位置。如果该位置为空,则返回 null。如果该位置不为空,则遍历链表或红黑树,查找对应的键值对。

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

6. 删除操作(remove)

        删除键值对时,计算键的哈希值,然后定位到数组的索引位置。如果该位置为空,则返回 null。如果该位置不为空,则遍历链表或红黑树,找到并移除对应的键值对。

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

7. 扩容(resize)

        当 HashMap 中的元素个数超过一定比例(负载因子)时,HashMap 会进行扩容,将数组容量加倍,并重新哈希所有键值对到新的数组中。这是一个代价较高的操作,但保证了 HashMap 的高效性。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap

8.说一下 HashMap 的实现原理?(面试)

        HashMap是一种常用的数据结构,特别是在需要高效查找、插入和删除操作时。它的实现主要依赖于哈希表(Hash Table)。以下是 HashMap的实现原理:

8.1. 哈希函数

        HashMap使用哈希函数将键(key)映射到一个桶(bucket)或槽位(slot)。哈希函数接受键并返回一个整数值,这个值用来定位存储数据的数组索引。

8.2. 数组

        哈希表的核心是一个数组。每个数组元素称为一个桶,存储键值对(key-value pair)。在 Java 中,初始数组的默认大小是 16。

8.3. 冲突处理

        由于不同的键可能映射到相同的数组索引,称为哈希冲突,HashMap需要一种机制来处理冲突。常见的冲突处理方法包括:

  • 链地址法(Chaining):每个桶中存储一个链表,当多个键映射到同一个桶时,这些键值对被存储在链表中。
  • 开放地址法(Open Addressing):如果发生冲突,寻找数组中的下一个空闲位置存储键值对。常见的策略包括线性探测(Linear Probing)、二次探测(Quadratic Probing)和双重哈希(Double Hashing)。
8.4. 负载因子和扩容

        负载因子(Load Factor)是哈希表已使用容量与总容量的比例。在 Java 的 HashMap实现中,默认负载因子是 0.75。当实际负载因子超过阈值时,HashMap会进行扩容,将数组大小翻倍,并重新哈希所有键值对到新的数组中。

8.5. 操作时间复杂度
  • 插入(Put):在理想情况下,时间复杂度为 O(1)。当发生冲突并且链表很长时,最坏情况时间复杂度为 O(n)。
  • 查找(Get):在理想情况下,时间复杂度为 O(1)。同样地,冲突处理不当时最坏情况时间复杂度为 O(n)。
  • 删除(Remove):在理想情况下,时间复杂度为 O(1)。冲突处理不当时最坏情况时间复杂度为 O(n)。
8.6. Java 中的 HashMap 细节

        在 Java 8 及之后的版本中,为了优化性能,当链表长度超过一定阈值(默认为 8)时,链表会转换为红黑树(Red-Black Tree),从而将查找、插入和删除的最坏情况时间复杂度从 O(n) 改善到 O(log n)。

9.总结

        HashMap 通过哈希函数将键映射到数组索引,并使用链地址法或开放地址法处理冲突。为了保持高效操作,HashMap使用负载因子和扩容机制,并在 Java 8 后引入了红黑树以优化长链表的性能。通过这些机制,HashMap提供了高效的插入、查找和删除操作。

 

 🌟感谢支持 听忆.-CSDN博客

🎈众口难调🎈从心就好

  • 46
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 25
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值