HashMap查找get()扩容resize()位运算解释

铺垫

1、a%2^{n}=a&(2^{n}-1)。即a与2的n次方的数取余,等于a与此值减一后的值按位取与。

2、a%2^{n}=x,则a%2^{n+1}=x,或者a%2^{n+1}=x+2^{n}

3、a%2^{n}=x,如果a&2^{n}=0,则a%2^{n+1}=x否则a%2^{n+1}=x+2^{n}

 

上述铺垫内容,在后期博客证明:

 

HashMap查找过程

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) {
            //由铺垫1得出(n-1)&hash相当于hash%n 根据key的hash值查找到key所在的数组下标
            

            //判断链表的第一个元素是否是待查找的数据
            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;
    }

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;
        //数组长度扩容2倍,阈值扩容2倍
        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; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //如果当前数组下边对应的链表只有一个元素,则直接用key ,hash对新扩容的数组取余,找到对应扩容数组的位置。
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //挨个遍历当前链表中的元素,根据新数组的长度重新分配数组位置
                        //根据铺垫2,犹豫新数组是原始素组长度的2倍,所以链表中的元素hash后取余,新位置要么不变,要么就是原始位置+原数组长度。
                        //根据铺垫3判断新元素链表位置是在原始位置还是在原始位置+原始数组长度位置
                        
                        //保存位置不变的链表元素
                        Node<K,V> loHead = null, loTail = null;
                        
                        //保存位置发生变化的链表元素,原始位置+原数组长度
                        Node<K,V> hiHead = null, hiTail = null;
                        
                        //head为链表的头。tail为链表的尾部,随着遍历元素,不断向尾部添加元素。

                        Node<K,V> next;
                        do {
                            next = e.next;
                            //根据铺垫3,判断链表元素位置是否发生变化
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //将位置不变的链表元素重新赋值到新的数组下标j
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //将位置发生变化的链表元素重新赋值到新的数组下标j+oldCap(原数组长度)
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

 

<think>嗯,用户问的是面试中手撕HashMap,我得先理解他们需要什么。手撕HashMap通常是指面试要求现场实现一个简化版的HashMap,考察对数据结构和算法的理解。那用户可能正在准备面试,需要知道如何应对这类问题。 首先,我应该回忆HashMap的基本原理。HashMap是基于哈希表实现的键值对存储结构,核心是哈希函数、解决哈希冲突的方法(比如链地址法或开放寻址法),以及扩容机制。接下来,用户可能需要知道实现的步骤和关键点。 接下来要考虑如何组织回答。通常,分步骤解释比较清晰。先讲哈希函数的设计,再讲解决冲突的方法,然后讲put、get、remove等核心方法,最后提扩容。可能需要给出代码示例,但用户提到要使用中文回答,并且注意LaTeX格式,所以代码部分可能需要用文字描述,或者用伪代码。 用户提到的系统级指令要求行内数学公式用$...$,独立公式用$$...$$,比如哈希函数中的取模运算应该用数学表达式。例如,index = hash(key) % capacity,这里hash(key)可能涉及哈希函数的计算,如多项式滚动哈希,或者直接使用Java的hashCode方法。需要确保这些部分正确使用LaTeX。 另外,用户可能需要了解常见的面试问题,比如如何处理哈希冲突,负载因子和扩容的关系,间复杂度分析等。在回答中应该覆盖这些点,并且结构清晰,分点说明。 还要注意可能的错误点,比如在扩容重新哈希所有元素,或者链表过长转换为红黑树(像Java 8那样)。不过手撕的话可能只需要实现链表法,不需要红黑树那么复杂。需要根据面试的要求调整复杂度,但通常简化版只要链地址法即可。 最后,给出一个示例代码的大纲,比如定义节点类,初始化桶数组,实现put、get等方法。同提醒注意事项,比如哈希函数的选择,处理null键的情况,以及扩容的触发条件。这样用户能有一个全面的理解,逐步实现自己的HashMap。</think>在面试中实现简化版HashMap是常见考察点,我将分步骤解析实现原理和关键代码逻辑: 一、核心原理 1. 哈希函数:通过$hash(key) \% capacity$计算存储位置 2. 冲突解决:常用链地址法(数组+链表),当链表长度>8可升级为红黑树(简化版可不实现) 3. 负载因子:默认0.75,当元素数量 > $capacity \times loadFactor$触发扩容 二、实现步骤 1. 定义节点类: ```java class Node<K,V> { final int hash; final K key; V value; Node<K,V> next; // 构造方法... } ``` 2. 初始化参数: ```java Node<K,V>[] table; // 存储桶数组 int capacity = 16; // 初始容量 float loadFactor = 0.75f; int threshold = (int)(capacity * loadFactor); // 扩容阈值 int size; // 当前元素数 ``` 3. 哈希函数实现(JDK风格): ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 三、关键方法实现 1. put方法核心逻辑: ``` public V put(K key, V value) { // 1. 计算哈希值 int hash = hash(key); int index = hash & (capacity-1); // 等价于取模(当capacity是2^n) // 2. 遍历链表查找是否存在相同key Node<K,V> first = table[index]; for (Node<K,V> e = first; e != null; e = e.next) { if (e.hash == hash && (e.key == key || (key != null && key.equals(e.key)))) { V oldValue = e.value; e.value = value; return oldValue; } } // 3. 插入新节点(头插法) table[index] = new Node<>(hash, key, value, first); // 4. 检查扩容 if (++size > threshold) resize(); return null; } ``` 2. 扩容实现要点: ```java void resize() { // 1. 新容量为原来的2倍 int newCap = capacity << 1; // 2. 创建新数组 Node<K,V>[] newTab = new Node[newCap]; // 3. 重新计算哈希分布 for (Node<K,V> head : table) { while (head != null) { Node<K,V> next = head.next; int newIndex = head.hash & (newCap - 1); head.next = newTab[newIndex]; newTab[newIndex] = head; head = next; } } // 4. 更新参数 table = newTab; capacity = newCap; threshold = (int)(newCap * loadFactor); } ``` 四、间复杂度分析 $$ \begin{cases} O(1) & \text{理想情况(无冲突)} \\ O(n) & \text{最坏情况(全冲突)} \end{cases} $$ 五、常见优化方向 1. 红黑树优化:当链表长度>8转为红黑树(间复杂度降为$O(\log n)$) 2. 缓存哈希值:避免重复计算 3. 延迟初始化:首次put再创建数组 4. 树化阈值检测:当桶数量<64优先扩容而非树化 六、面试考察重点 1. 哈希函数设计原理(高低位异或的作用) 2. 为什么选择2的幂次作为容量(位运算代替取模) 3. 头插法vs尾插法的选择(并发环境下可能产生循环链表) 4. 线程安全问题的理解(ConcurrentHashMap的分段锁原理) 建议在实现优先保证基础功能正确,再逐步添加优化点。注意处理null键的特殊情况,并验证边界条件(如扩容前后数据一致性)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

菜鸟long

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

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

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

打赏作者

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

抵扣说明:

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

余额充值