关于HashMap令面试官眼前一亮的回答

面试中的HashMap实现

为什么要深入了解HashMap的内部实现

HashMap是互联网公司面试最频繁的知识点之一,而很少问及List的的实现类,与因为List的实现过于简单,所以掌握HashMap的原理会让你在面试中获得先机。

哈希表

哈希表(hash table)也叫散列表,是一种非常重要的数据结构,在通用的数据结构中,哈希表的增删改查速度是最快的,哈希表存储的是键值对,其查找的时间复杂度和元素数量无关,哈希表在查找元素时是通过计算哈希码值来定位元素的位置从而直接访问元素的,因此哈希表查找(不考虑哈希冲突的情况下)的时间复杂度为O(1);应用场景:数据量较小;不需要预先知道数据规模;适应于频繁的插入操作。

哈希冲突

然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

HashMap实现原理

HashMap的主干是一个Entry数组,每个Entry的包含一个键值对key-value;简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

HashMap的get和put方法的具体实现

以下是具体的put过程(JDK1.8版)
1、对Key求Hash值,然后再计算下标

2、如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)

3、如果碰撞了,以链表的方式链接到后面

4、如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表

5、如果节点已经存在就替换旧值

6、如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)

以下是具体get过程(考虑特殊情况如果两个键的hashcode相同,你如何获取值对象?)
当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

.如何减少碰撞

1扰动函数可以减少碰撞;如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小很多,这就意味着存链表的结构减小,这样取值的时候就不会频繁的调用的equal方法,即可提高HashMap的性能
2使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。为什么String, Interger这样的wrapper类适合作为键?因为String是final的,而且已经重写了equals()和hashCode()方法了。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。
3 JDK 1.8的hash()方法实现源码

  static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

**>>>**无符号右移 无符号右移规则和右移运算是一样的,只是填充时不管左边的数字是正是负都用0来填充,无符号右移运算只针对负数计算,因为对于正数来说这种运算没有意义
^ 位异或运算(^)

拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?

之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于***8***的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。这个值只可能在两个地方,一个是原下标的位置,另一种是在下标为<原下标+原容量>的位置

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMap是一种散列表,它存储的内容是键值对(key-value)映射。当我们将一个键值对存储在HashMap,它会根据键的哈希码找到对应的桶(bucket),然后将键值对存储在桶中。当我们需要获取一个键对应的值HashMap会根据键的哈希码找到对应的桶,然后在桶中查找对应的值。 在面试中,如果被问到HashMap是否有序,我们需要回答HashMap不是有序的,因为HashMap中的键值对是根据哈希码存储的,而哈希码是无序的。如果被问到HashMap的存储原理,我们需要回答HashMap是通过哈希算法将键映射到桶中的,然后将键值对存储在桶中。如果被问到哈希算法的底层实现,我们需要回答说哈希算法的底层实现是哈希表。 以下是一个简单的HashMap实现代码,仅供参考: ```java public class MyHashMap<K, V> { private static final int DEFAULT_CAPACITY = 16; private static final float DEFAULT_LOAD_FACTOR = 0.75f; private Entry<K, V>[] table; private int size; private int capacity; private float loadFactor; public MyHashMap() { this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); } public MyHashMap(int capacity, float loadFactor) { this.capacity = capacity; this.loadFactor = loadFactor; this.table = new Entry[capacity]; } public void put(K key, V value) { int index = hash(key); Entry<K, V> entry = table[index]; while (entry != null) { if (entry.key.equals(key)) { entry.value = value; return; } entry = entry.next; } Entry<K, V> newEntry = new Entry<>(key, value); newEntry.next = table[index]; table[index] = newEntry; size++; if (size > capacity * loadFactor) { resize(); } } public V get(K key) { int index = hash(key); Entry<K, V> entry = table[index]; while (entry != null) { if (entry.key.equals(key)) { return entry.value; } entry = entry.next; } return null; } private int hash(K key) { return key.hashCode() % capacity; } private void resize() { capacity *= 2; Entry<K, V>[] newTable = new Entry[capacity]; for (Entry<K, V> entry : table) { while (entry != null) { Entry<K, V> next = entry.next; int index = hash(entry.key); entry.next = newTable[index]; newTable[index] = entry; entry = next; } } table = newTable; } private static class Entry<K, V> { K key; V value; Entry<K, V> next; public Entry(K key, V value) { this.key = key; this.value = value; } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值