HashMap

HashMap

概述

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。

HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

“拉链法”解决哈希冲突

参考博客:https://blog.csdn.net/qq_32595453/article/details/80660676

hash : 翻译为“散列”,就是把任意长度的输入,通过散列算法,变成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值,由此引出hash冲突。

hash冲突就是键(key)经过hash函数得到的结果作为地址去存放当前的键值对(key-value)(这个是hashmap的存值方式),但是却发现该地址已经有人先来了,一山不容二虎,就会产生冲突。这个冲突就是hash冲突了。

一句话说就是:如果两个不同对象的hashCode相同,这种现象称为hash冲突。

HashMap,HashSet其实都是采用的拉链法来解决哈希冲突的,就是在每个位桶实现的时候,我们采用链表(jdk1.8之后采用链表+红黑树)的数据结构来去存取发生哈希冲突的输入域的关键字(也就是被哈希函数映射到同一个位桶上的关键字)。首先来看使用拉链法解决哈希冲突的几个操作:

①插入操作:在发生哈希冲突的时候,我们输入域的关键字去映射到位桶(实际上是实现位桶的这个数据结构,链表或者红黑树)中去的时候,我们先检查带插入元素x是否出现在表中,很明显,这个查找所用的次数不会超过装载因子(n/m:n为输入域的关键字个数,m为位桶的数目),它是个常数,所以插入操作的最坏时间复杂度为O(1)的。

②查询操作:和①一样,在发生哈希冲突的时候,我们去检索的时间复杂度不会超过装载因子,也就是检索数据的时间复杂度也是O(1)的

③删除操作:如果在拉链法中我们想要使用链表这种数据结构来实现位桶,那么这个链表一定是双向链表,因为在删除一个元素x的时候,需要更改x的前驱元素的next指针的属性,把x从链表中删除。这个操作的时间复杂度也是O(1)的。

拉链法的优点

与开放定址法相比,拉链法有如下几个优点:

①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

拉链法的缺点

指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

put函数

put函数大致的思路为:

  1. 对key的hashCode()做hash,然后再计算index;

  2. 如果没碰撞直接放到bucket里;

  3. 如果碰撞了,以链表的形式存在buckets后;

  4. 如果碰撞导致链表过长(大于等于 TREEIFY_THRESHOLD ),就把链表转换成红黑树;

(红黑树:http://www.cnblogs.com/skywang12345/p/3245399.html)

  1. 如果节点已经存在就替换old value(保证key的唯一性)

  2. 如果bucket满了(超过 load factor*current capacity ),就要resize。

具体代码的实现如下:

public V put(K key, V value) { 

// 对key的hashCode()做hash 

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; 

// tab为空则创建 

if ((tab = table) == null || (n = tab.length) == 0) 

n = (tab = resize()).length; 

// 计算index,并对null做处理 

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, k 

ey, 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.e 

quals(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; 

// 超过load factor*current capacity,resize 

if (++size > threshold) 

resize(); 

afterNodeInsertion(evict); 

return null; 

}

get函数

大致思路如下:

  1. bucket里的第一个节点,直接命中;

  2. 如果有冲突,则通过key.equals(k)去查找对应的entry

    若为树,则在树中通过key.equals(k)查找,O(logn);

    若为链表,则在链表中通过key.equals(k)查找,O(n)。

具体代码的实现如下:

public V get(Object key) { 

Node<K,V> e; 

return (e = getNode(hash(key), key)) == null ? null : e.valu 

e;

}

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.equal 

s(k)))) 

return first; 

// 未命中 

if ((e = first.next) != null) { 

// 在树中get 

if (first instanceof TreeNode) 

return ((TreeNode<K,V>)first).getTreeNode(hash, 

key); 

// 在链表中get 

do {

if (e.hash == hash && 

((k = e.key) == key || (key != null && key.e 

quals(k)))) 

return e; 

} while ((e = e.next) != null); 

} 

}

return null; 

} 

总结

1. 什么时候会使用HashMap?他有什么特点?

是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。

2. 你知道HashMap的工作原理吗?

通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过 Load Facotr 则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket

位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

3. 你知道getput的原理吗?**equals()hashCode()**的都有什么作用?

通过对key的hashCode()进行hashing,并计算下标( (n-1) & hash ),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点

4. 你知道hash的实现吗?为什么要这样实现?

在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的: (h =k.hashCode()) ^ (h >>> 16) ,主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。

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

如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值