HashMap常见面试题总结

9 篇文章 0 订阅
8 篇文章 0 订阅

目录

 

什么是HashMap?

你为什么要用HashMap?

HashMap的数据结构?

HashMap的工作原理?

HashMap key的存储下标是怎么计算的?

HashMap为什么速度快?/为什么要使用hashcode?

怎样解决Hash冲突?

HashMap怎么解决hash冲突?

HashMap什么时候开辟数组,占用内存

初始化,构造函数?

HashMap的初始容量和加载因子如果自己设置的话,设置什么比较好呢

HashMap put()的步骤

扩容原理?/如果HashMap超过了负载因子定义的容量会怎么办?

怎样进行重构hash表

重新调整HashMap的大小存在什么问题?

多线程并发情况下使用HashMap可能会存在什么问题?

HashMap怎样转换为线程安全的?(三种方法)

可以使用ConcurrentHashMap来代替HashTable吗?

介绍ConcurrentHashMap

为什么String,Integer这样的warpper类更适合作为键

可以使用自定义的对象作为键吗

HashMap在jdk1.8之后添加了什么?

HashMap的遍历


什么是HashMap?

HashMap是一个存储key-value的键值对集合,每一个元素都是一个Entry,这些键值对分散在数组当中。

你为什么要用HashMap?

  1. 解决问题需要的数据结构是一种键值对的数据结构
  2. HashMap是线程不安全的,其速度比较快
  3. HashMap在存储key的值时,允许为NULL
  4. 对于输入数据的顺序与输出数据的顺序没有特别要求(如果有特别要求,要用LinkedHashMap)

Hash函数的构造方法?

Hash函数的构造原则是简单和均匀,一般用到的hash()函数的构造方法有:

1.除留余数法  H(key)=key%p  p是小于等于表长的最大素数

2.数字分析法  假设每个关键字都是由s位组成的,如果可以预先估算出全体关键字每一位数字输出的频率,可以从中提取出分布均匀的若干位,或者他们的组合作为hash地址

3.平方取中法  对于每一个数字,先平方,然后同2方法。采用平方是为了扩大相近数的差别。

4.分段叠加法 将关键字分割成位数相同的几部分,(最后一部分的位数可以不同),然后取这几部分进行叠加,叠加和舍去进位作为散列地址。

5.基数转换法 将关键字看成是另一种进制的函数,最后转换为本进制的函数,选择其中几位作为散列地址。

HashMap的数据结构?

数组+链表+红黑树

一个节点存放四个值:hash值,key值,value值,next执行该条链的下一个节点的指针。

HashMap的工作原理?

HashMap基于哈希原理。当我们通过put()方法和get()方法存取元素时,将键值对传给put方法时,通过调用hash方法得到hash值,然后计算出在数组中存储的位置,找到相应的桶的位置,将对象[entry]进行存储。如果key已经存在,value值会更新(hashMap不存放key重复的值)。用拉链法解决hash冲突。在获取对象时,通过hash值&(length-1),找到存储位置,通过equals方法,来确定是否找到

HashMap key的存储下标是怎么计算的?

用的哈希算法,首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)计算得到存储的位置

计算hash值:

(h = key.hashCode())^(h >>> 16);简而言之,hashcode的值异或hashcode无符号右移16位的值

为什么要hashcode要异或其右移十六位的值?

对于计算出来的两个hashcode,如果其高位不同,低位基本相同,&(length-1)之后,hash碰撞的机会也别大,采用位运算,将其分散的更为均匀。

为什么hash值要与length-1相与?

Hash值要在数组中找一个对应的长度,本应该用hash值对数组的长度进行取模运算,但是hash值比较大,除数运算较为复杂,所以用hash值与length-1相与,取得一个在0到length-1之间的值,作为数组存储的下标。

HashMap存储的时候数组的长度为什么要是2的n次幂?

为了均匀分布,如果低位是1,相与之后得到的结果是尽可能多的。否则,有的位永远取不到,浪费了空间,增大了冲突,减慢了查询效率

HashMap为什么速度快?/为什么要使用hashcode?

取的时间复杂度是o(1),通过散列表取元素,根据hash值&(length-1),找到存储位置,如果是链表的话,就要在链表中查找,但是链表的查询时间复杂度为o(n),这是为了让hashmap的存储时间复杂度变为o(1),就要是hash冲突尽可能减少,采用了空间换时间的策略,在理想的状态下,取的时间复杂度为o(1),存储也是同样的。

怎样解决Hash冲突?

参考:https://www.cnblogs.com/wuchaodzxx/p/7396599.html

四种方法:

  1. 开放定址法

1.1线性探测再散列:

    冲突发生时,顺序查看表中下一个单元,直到找到一个空单元,或查遍全表。表后面查完,如果没有,在表头继续查看(相当于是一个循环),步长为1.

1.2二次线性再散列

    冲突发生时,分别在表的左右进行跳跃式探测,较为灵活,不易产生聚集,但缺点是不能探测到整个散列的地址空间。步长是1,-1,4,-4…

1.3伪随机探测再散列

    建立一个随机数发生器,并给定一个随机数作为起始点。(随机数序列是    预先产生的)步长是一个随机序列。

2.拉链法

    把所有具有地址冲突的关键字链在同一个链表中。

3.再哈希法

       构造多个hash函数,当发生冲突时,用其他的哈希函数,产生hash值,   直到不冲突为止。

4.建立公共溢出区

        将哈希表分为基本表和溢出表两个部分,凡是和基本表发生冲突的,一 律填入溢出表。

HashMap怎么解决hash冲突?

采用拉链法(链地址法)。当好多bin被映射到同一个桶时,如果桶中bin<6,采用链表存储,如果bin>8但是Hash表的容量小于64,依然用链表存储,对hash表进行扩容。,然后进行hash表的重构。如果bin>8并且Hash表的容量大于64,则将链表的结构转换为红黑树的结构进行存储。

HashMap什么时候开辟数组,占用内存

第一次put的时候,而不是new的时候,在put的时候调用resize方法来开辟数组

初始化,构造函数?

Hashmap一共有四个构造函数

HashMap()没有指定时,使用默认的初始化大小16,使用默认的加载因子0.75

HashMap(int inititalCapacity)

HashMap(int inititalCapacity,float loadFactor)

指定初始化大小,hashmap调用tablesize,找到大于指定初始化容量大小的最小二次幂值

int n=cap-1;

n|=n>>>1;

n|=n>>>2;

n|=n>>>4;

n|=n>>>8;

n|=n>>>16;

返回n+1表示初始的大小

table初始化不是在初始化时完成的,而是在resize的时候完成的。

HashMap的初始容量和加载因子如果自己设置的话,设置什么比较好呢

扩容非常耗时,我们在设计初始容量时,应该尽可能避免这种情况。一般情况下:

(需要的数组容量)/加载因子+1;

加载因子默认是0.75,如果加载因子过小的话,就会浪费内存,如果加载因子过大的话,就会产生hash冲突比较多

 

HashMap put()的步骤

1.首先根据key的值计算hash值,找到该元素在数组中存储的下标

2.如果数组是空的,则调用resize进行初始化

3.如果没有碰撞直接放在对应的数组下标里

4.如果碰撞了,且节点已经存在,就替换掉value

5.如果碰撞后,发现该节点是树结构,就将这个节点挂在树上

6.如果碰撞后是链表,就把节点添加到链表的表尾,然后判断该链表是否>8,如果大于8但是数组容量小于64,就进行扩容

7.如果链表节点大于8并且数组的容量大于64,则将这个结构转换为树形结构

扩容原理?/如果HashMap超过了负载因子定义的容量会怎么办?

Hashmap在容量超过负载因子所定义的容量之后,就会扩容。Hashmap的默认负载因子是0.75.阈值=0.75*容量,也就是说当前hashmap存储的容量超过负载因子的容量就会扩容。这时就会将hashmap的大小扩大为原来数组的两倍。并将原来的对象放入新的数组中。这个过程叫做再hash,因为他调用hash方法来确定。

 

怎样进行重构hash表

https://segmentfault.com/a/1190000015812438

扩容时,要进行hash表的重构,对于一个链上挂了多个节点的情况,遍历这条链表,对于每个节点的处理方法如下:

建立两条链表,lo和hi, 如果当前节点的hash值&oldCap(原数组的容量)==0,将其挂在lo链表上,如果hash&oldCap(原数组的容量)==1,将其挂在hi链表上。最后将lo链表存储在j的位置,就是原来的位置,将hi链表存储在j+oldCap的位置。

原理:

假设数组之前是16位,0000 0000 0000 1111&hash值,相当于取hash值的第四位,当数组扩容之后32位,0000 0000 0001 1111&hash值取得是hash值的低五位,新的存储位置与原来的存储位置相比较,只有第五位发生了变化,第五位要么是0要么是1,这就使得新的结果要么和原来的结果相同,要么在原来的结果上加10000,也就是加上oldCap的值。新旧位置的不同在于hash值的第4位不同,(0是最低位),这个时候要拿到这一位,就可以hash&oldCap,如果相同,如果结果是0,则还在原来的位置存放。如果结果不相同,在原来的位置上+oldCap

重新调整HashMap的大小存在什么问题?

https://www.cnblogs.com/andy-zhou/p/5402984.html#_caption_0

在多线程的环境下存在条件竞争。如果两个线程都发现HashMap需要调整大小,那么他们会同时尝试调整大小。就有可能出现环形链表,导致程序出现死循环,(不是死锁)

因为jdk1.8之前链表的插入用的是头插法,所以在多线程的环境下有可能导致环形链表的出现,而jdk1.8之后链表的插入用的是尾插,不会出现这个问题,所以jdk1.8之后hashmap扩容不会出现死循环。

多线程并发情况下使用HashMap可能会存在什么问题?

  1. 多线程同时操作put()方法可能会导致get()操作发生死循换,环形链表的出现[jdk1.8之后没有这个问题]
  2. 多线程put()操作导致元素丢失

如果两个线程都同时获得了e,指向该链表的尾指针,则各自执行各自的,最后执行的覆盖原来的,另外一个线程所做的更改就会丢失。

  1. put非null元素之后get出来的却是null。

线程1在重建表的时候会把原来的数组中的元素置空,而线程2在刚开始的时候得到的却是原数组,这是原数组被置空,线程2得到的就是null

HashMap怎样转换为线程安全的?(三种方法)

  1. 用HashTable替换HashMap
  2. Collections.synchronizedMap将HashMap包装起来,里面所有的方法都加上了同步锁
  3. ConcurrentHashMap替换HashMap

可以使用ConcurrentHashMap来代替HashTable吗?

Concurrent可以代替HashTable,HashTable可以提供更强的安全性,因为每一个方法都用了同步锁,但是在线程竞争激烈的情况下,HashTable的性能非常低下,ConcurrentHashMap的同步性能较好,因为他是根据map的性能级别,对于map中的一部分进行上锁。

介绍ConcurrentHashMap

https://blog.csdn.net/qq_27093465/article/details/52279473

Java.util.concurrentHashMap 

采用了分段锁技术。首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据的时候,其他段的数据也能被线程访问。

ConcurrentHashMap是由segment数组和HashEntry数组组成,Segment是一种可重用锁ReenTrantLock扮演锁的角色,HashEntry用于存储键值对数据。一个ConcurrentHashMap里面包含一个segment数组。一个segment中包含一个HashEntry数组。

为什么String,Integer这样的warpper类更适合作为键

String,Integer这些类不可变,是final的,其hashcode和equals方法重写了。其他的wrapper类也有这个特点,如果hashcode值是可变的,就不能从HashMap中找到你想要的对象。

可以使用自定义的对象作为键吗

可以,可以使用任何对象作为键,只要他遵循equals和hashcode方法的规则

为什么大部分hashcode方法使用31?

31是一个奇素数,如果是偶数的话,会产生溢出。习惯上在散列中用素数。31有一个很好的性能,用移位和减法来代替乘法,可以得到很好的性能。31*i=i<<5-i;

 

HashMap在jdk1.8之后添加了什么?

  1. Jdk1.7之前是数据每个节点声明是Entry类型的,jdk后声明为Node类型,成员基本没变,只是把hash声明为了final
  2. 在一定情况下,拉链链表中的元素超过8个时,采用红黑树
  3. 扩容时,不用重新结算位置,要么在原位置,要么在原位置的基础上移动二次幂的位置
  4. 链表的插入用尾插法,jdk1.7链表插入用头插,多并发的情况下容易形成环形链表,造成死循环。用尾插法解决了这个问题。
  5. Jdk1.8的hash方法在求得hash值得同时,将其与右移16位的结果异或,高低位都参与运算,是的hash结果更为均匀。

HashMap的遍历

public class Demo05 {
    public static void main(String[] args) {
        //HashMap遍历的两种办法
        Map map=new HashMap();
        map.put(1,7);
        map.put(2,9);
        map.put(3,3);
        Iterator it1=map.entrySet().iterator();
        while (it1.hasNext())
        {
            Map.Entry entry=(Map.Entry)it1.next();
            Object key=entry.getKey();
            Object value=entry.getValue();
            System.out.println(key+" "+value);
        }
        Iterator it2=map.keySet().iterator();
        while(it2.hasNext())
        {
            Object key=it2.next();
            Object value=map.get(key);
            System.out.println(key+" "+value);
        }
    }
}

第一种效率高且推荐使用

因为HashMap的这两种遍历分别是对keySet和EntrySet进行迭代,对于keySet实质上是遍历了两次,一次转为itertor迭代器遍历,一次就从HashMap中取出key所对应的value进行操作。(通过key值hashcode和equals索引),而entrySet方式只遍历了一次,他把key和value都放在了entry中,所以效率高

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值