hashmap面试题

hashmap1.8中的hash函数

        简单的说就是对key做hashCode操作,然后将得到的32为散列值向右位移16位,再与hashCode做异或计算。实质上是把一个数的低16位与他的高16位做异或运算

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

        首先 h = key.hashCode()是key对象的一个hashCode,每个不同的对象其哈希值都不相同,其实底层是对象的内存地址的32位的散列值,h >>> 16的意思是将hashcode右移16位,然后高位补0,然后再与(h = key.hashCode()) 异或运算得到最终的h值。

        为什么是异或运算呢?当然我们知道目的是为了让h的低16位更有散列性,但为什么是异或运算就更有散列性呢?而不是与运算或者或运算呢?网上大多的文章都没有给出一个很好的说明,这里我将证明一下为什么异或就能够得到更好散列性。

3.为什么异或运算的散列性更好?

先来看一下下面的这组运算

【与运算 1&0=0, 0&0=0, 0&1=0 都等于0 1&1=1 3次0,1次1】

【或运算 1&0=1, 1&1=1, 0&1=1 都等于1 0&0=0 3次1,1次0】

【异或运算 0&0=0, 1&1=0,而另外0&1=1, 1&0=1 2次1,2次0】

hashmap面试题_第1张图片

        上面是将0110和0101分别进行与、或、异或三种运算得到不同的结果,我们主要来看计算的过程:

        与运算:其中1&1=1,其他三种情况1&0=0, 0&0=0, 0&1=0 都等于0,可以看到与运算的结果更多趋向于0,这种散列效果就不好了,运算结果会比较集中在小的值

        或运算:其中0&0=0,其他三种情况 1&0=1, 1&1=1, 0&1=1 都等于1,可以看到或运算的结果更多趋向于1,散列效果也不好,运算结果会比较集中在大的值

        异或运算:其中0&0=0, 1&1=0,而另外0&1=1, 1&0=1 ,可以看到异或运算结果等于1和0的概率是一样的,这种运算结果出来当然就比较分散均匀了

        总的来说,与运算的结果趋向于得到小的值,或运算的结果趋向于得到大的值,异或运算的结果大小值比较均匀分散,这就是我们想要的结果,这也解释了为什么要用异或运算,因为通过异或运算得到的h值会更加分散,进而 h & (length-1)得到的index也会更加分散,哈希冲突也就更少。

3.2.为什么使用 hash & (length - 1) 作为数组的寻址算法?

        首先我们如果把数据存在一个数组中,我们会使用数组中的值hash % length取模操作,为每个值寻找存在数组中的位置,但是这种取模的操作性能不是很好,比起位运算差远了,后来发现当数组的容是2的n次方的时候hash & (length - 1) == hash % length,所以就使用hash & (length - 1) 来替代取模运算,这样操作效率高,而且数据均匀分布,hash碰撞少。

使用hash & (length - 1) 作为寻址算法也是jdk1.8的优化。

寻址算法的优化:使用与运算替代取模,提升性能。

那为什么要用数组值的hash值的高16与它的低16做异或呢?

        首先我们的寻址算法优化了,是使用hash & (length - 1) ,假设我们不适用新的优化后的hash算法,我们就直接使用数组中的值的hashcode,不使用高16与低16做异或,因为n-1的值通常是很小的,n-1通常高16为都是0,那么这个hash的高16为和n-1做与运算,hash的高16位就不起作用了,就相当于与之与两个都是低16位的值做与预算,而我们的目的就是为了hash更加散列,很少甚至不起hash冲突。所以如果使用hash值的高16与低16做异或,让他的低16为同时保持了高低16为的特征,尽量避免了hash冲突。

4.HashMap的容量为什么建议是2的幂次方?

关键就在于把当前数据存放到哪一个桶中,这个算法就是取模运算。

假设:

length:HashMap的容量

hash:当前key的哈希值

取模运算为 hash % length

        但是,在计算机中,直接取模运算的效率不如位运算(&),什么是位运算?就是对于二进制数据的按位运算,1和1才得1,其他都得0,比如:1011 & 1100 = 1000

        sun公司的大牛们发现,当容量为2的n次方时,hash & (length - 1) == hash % length ,于是就在源码中做了优化,通过 hash & (length - 1) 来替代取模运算,而前提就是容量必须为2的n次方。这样做的好处在于:

1. 提高操作运算效率(位运算效率 > 取模运算效率)

2. 减少碰撞,数据均匀分布,提高HashMap查询效率

为什么可以减少碰撞?举个例子,现在两个hash分别是2和3,:

比如 length 为 9 的情况:3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;

比如 length 为 8 的情况:3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;

5.为什么不采用AVL树或B树,B+树?

红黑树和AVL树都是最常用的平衡二叉搜索树。

但是,两者之间有些许不同:

        AVL树更加严格平衡,因此可以提供更快的査找效果。因此,对于查找密集型任务使用AVL树没毛病。 但是对于插入密集型任务,红黑树要好一些。

通常,AVL树的旋转比红黑树的旋转更难实现和调试。

红黑树更通用,再添加删除来说表现较好,AVL虽能提升一些速度但是代价太大了。

而不用B/B+树的原因:

        B和B+树主要用于数据存储在磁盘上的场景,比如数据库索引就是用B+树实现的。这两种数据结构的特点就是树比较矮胖,每个结点存放一个磁盘大小的数据,这样一次可以把一个磁盘的数据读入内存,减少磁盘转动的耗时,提高效率。而红黑树多用于内存中排序,也就是内部排序。

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

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

7.为什么树化阈值是8?为什么树退化为链表的阈值是6?

        根据泊松分布。当我们计算的哈希冲突到了8次,概率就非常小了,可以看到当链表长度为8时候的概率为千万分之6,概率极低。所以取该值作为树化阈值。

为什么取6不取7呢?

        链表转变为红黑树,和红黑树转变为链表,有很大的性能消耗。至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生。

8.loadFactor为什么是0.75?

负载因子是1.0

        当负载因子是1.0的时候,也就意味着,只有当数组的8个值(这个图表示了8个)全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的。当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。

因此一句话总结就是负载因子过大,虽然空间利用率上去了,但是时间效率降低了。

负载因子是0.5

        负载因子是0.5的时候,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低。查询效率就会增加。

但是,兄弟们,这时候空间利用率就会大大的降低,原本存储1M的数据,现在就意味着需要2M的空间。

一句话总结就是负载因子太小,虽然时间效率提升了,但是空间利用率降低了。

负载因子0.75

        经过前面的分析,基本上为什么是0.75的答案也就出来了,这是时间和空间的权衡。当然这个答案不是我自己想出来的。答案就在源码上,我们可以看看:大致意思就是说负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。

9.那构造时传10000是否能让HashMap存10000条数据不需要动态扩容呢?

        当我们构造传10000时,实际上经过 tableSizeFor() 方法处理之后,就会变成 2 的 14 次幂 16384,再算上负载因子 0.75f,实际在不触发扩容的前提下,可存储的数据容量是 12288(16384 * 0.75f)。完全可以存储10000条数据。

10.hashmap为什么要重写hashcode和equls方法?

        equals()和hashCode()这两个方法都是从Object类继承过来的,比较的是对象的地址值,如果你现在需要利用对象里面的值来判断是否相等,则重载equal方法。

        如果两个对象是相等(equals)的,那么两个对象调用hashCode必须产生相同的整数结果,即:equals为true,hashCode必须为true,equals为false,hashCode也必须为false,所以必须重写hashCode来保证与equal同步。

11.如何能提示HashMap的性能

        预设hashmap的长度,避免resize.

12.java hashMap和redis map的rehash有什么区别?

        今天在看Redis的字典rehash时,发现其与Java不一样;字典rehash底层实现与Java区别.

        ① java的hashmap采用的是集中式重新rehash,非常耗性能的

        ② Redis既然采用的是渐进式rehash

java的hashmap:

        对key做hashcode操作,得到的32为散列值向右位移16位,再与hashCode做异或计算。实质上是把一个数的低16位与他的高16位做异或运算。如果出现hash冲突,用链表或者红黑树解决。

redis:

Redis的扩容和收缩,是利用两个哈希表来完成。在设计上就和Java有区别;

typedef struct dict {
	dictType *type;
	void * privdata;
	// 哈希表
	dictht ht[2];
	// rehash索引,当rehash不在进行时,值为-1
	int rehashidx;
} dict;

        Redis字典创建时就有两个哈希表,不rehash的情况下,只使用ht[0]这个哈希表,当发生扩容和收缩时,才会用到ht[1]哈希表和rehashidx这个属性;

Redis采用的是渐进式过程,其重新计算hash和数据搬运的过程是发生在增删改查的操作中的,而不是集中一次性完成的。

rehash的过程:

        ① 为ht[1]分配空间,空间大小为旧空间的2n;

        ② 将字典中维持的索引计数器rehashidx设置0,表示rehash工作正式开始;

        ③ 在rehash期间,每次对字典的增、删、改、查,操作,程序除了执行指定的操作之外,还会顺带将ht[0]哈希表在rehashidex索引上的所有键值对rehash到ht[1],当完成后,程序将rehashidex的值加一。

        ④ 随着字典操作的不断进行,ht[0]表中的所有键值对都会被rehash到ht[1],这时会将rehashidx属性值设置为-1,表示rehash操作结束。hashmap面试题_第2张图片

13.为什么计算hash值要右移16位不是17位?

        首先hash返回值是int类型的数值是4个字节的,一个字节由8个二进制位组成,那就是32个二进制位组成,如果拿16位,正好是高16位,低16位做异或运算,这样算出来的比较均匀分散,减少碰撞,进一步降低hash冲突的几率。

14.hashmap插入删除数据,时间复杂度是多少?

        因为HashMap很少出现hash冲突了,因为哈希算法足够优秀,那么全是o(1)

当有链表的时候,那么就是o(n)的复杂度

转成红黑树 也就是二叉树的一种,那么应该是o(logN)的平均复杂度

15.HashMap 与 HashTable区别?

        1.hashmap:线程不安全,hashtable:线程安全

        2.hashmap:允许有null的键和值, hashtable:不允许有null的键和值

        3.HashTable 初始默认大小是11,增加的方式是 old*2+1。HashMap初始默认大小是16,而且一定是2的指数。

16.java中为什么HashTable的K-V不能是null?为什么ConcurrentHashMap key 不可以为null ?

        HashMap中的key-value可以是null,为什么HashTable和ConcurrentHashMap中的不可以是null呢?

源码:

if (value == null) {
    throw new NullPointerException();
}

        从HashTable源代码put方法可以看出,首先会对put方法中的值value进行null值判断,为null抛出空指针异常;然后会对key值进行Object类的hashCode方法计算hash值,为null值也会抛出空指针异常,故不能在HashTable键、值中放入null值。

        因为 Hashtable 采用了安全失败机制(fail-safe),导致当前得到的数据不一定是集合最新的数据。

        如果使用null值,就不能判断到底是映射的value是null,还是因为没有找到对应的key而为空,因为你无法再调用contain(key)来对key是否存在做判断,ConcurrentHashMap同理。在多线程情况下,即便此刻你能通过contains(key)知晓了是否包含null,下一步当你使用这个结果去做一些事情时可能其他并发线程已经改变了这种状态,而这对于用于单线程状态的hashmap是不可能发生的,它可以用contains(key) 去判断到底是否包含了这个null,从而做相应处理。

ConcurrentHashMap不能为null原因,无法去判断到底是key不存在还是value为null。

17.hashmap初始大小是16;最大是2的30次方;超过最大值如何处理?

        HashMap的底层实现,发现HashMap的最大容量规定为 必须是2的幂且小于2的30次方,传入容量过大将被这个值替换。

18.hashmap的最大值为什么会是2的30次幂,而不是2的31次幂呢?

        由于int类型限制了该变量的长度为4个字节共32个二进制位,按理说可以向左移动31位即2的31次幂。但是事实上由于二进制数字中最高的一位也就是最左边的一位是符号位,用来表示正负之分(0为正,1为负),所以只能向左移动30位,而不能移动到处在最高位的符号位!

        首先:JAVA规定了该static final 类型的静态变量为int类型,至于为什么不是byte、long等类型,原因是由于考虑到HashMap的性能问题而作的折中处理!

        如果我要存的数目大于 MAXIMUM_CAPACITY,你还把我的容量缩小成 MAXIMUM_CAPACITY???

在这里我们可以看到其实 hashmap的“最大容量“是Integer.MAX_VALUE;

19.hashmap resize是否要重新计算hash值?

        不需要,我们在扩容的时候,一般是把长度扩为原来2倍,所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

20.HashSet的value是什么?HashSet为什么不存null?

hashmap面试题_第3张图片

 查看PRESENT是什么

        HashSet底层其实就是维护着HashMap,HashSet的add方法,其实是将值,作为HashMap的key存入。而如果HashSet的value 用null值的话,那么在删除操作的时候,可能remove方法返回的值就永远都是true了。

        所以HashSet用对象类型作为value,来保证删除的key的value值都是不一样的,这样就可以保证上述remove方法返回的正确性!

        而map的移除会返回value,如果底层value都是存null,

显然将无法分辨是否移除成功

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