Hashtable 初始容量是11 ,扩容 方式为2N+1;

HashMap 初始容量是16,扩容方式为2N;  

阿里的人突然问我为啥扩容因子是0.75,回来总结了一下; 提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小,

HashMap有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行扩容、rehash操作(即重建内部数据结构),扩容后的哈希表将具有两倍的原容量。

通常,加载因子需要在时间和空间成本上寻求一种折衷。

加载因子过高,例如为1,虽然减少了空间开销,提高了空间利用率,但同时也增加了查询时间成本;

加载因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了rehash操作的次数

在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少rehash操作次数,所以,一般在使用HashMap时建议根据预估值设置初始容量,减少扩容操作。

选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择, 

正文

前几天在一个群里看到有人讨论hashmap中的加载因子为什么是默认0.75。

HashMap源码中的加载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;  

当时想到的是应该是“哈希冲突”和“空间利用率”矛盾的一个折衷。

跟数据结构要么查询快要么插入快一个道理,hashmap就是一个插入慢、查询快的数据结构。

加载因子是表示Hash表中元素的填满的程度。
加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。
反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了。

冲突的机会越大,则查找的成本越高。反之,查找的成本越小。

因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。

哈希冲突主要与两个因素有关,(1)填装因子,填装因子是指哈希表中已存入的数据元素个数与哈希地址空间的大小的比值,a=n/m ; a越小,冲突的可能性就越小,相反则冲突可能性较大;但是a越小空间利用率也就越小,a越大,空间利用率越高,为了兼顾哈希冲突和存储空间利用率,通常将a控制在0.6-0.9之间,而.net中的HashTable则直接将a的最大值定义为0.72 (虽然微软官方MSDN中声明HashTable默认填装因子为1.0,但实际上都是0.72的倍数),(2)与所用的哈希函数有关,如果哈希函数得当,就可以使哈希地址尽可能的均匀分布在哈希地址空间上,从而减少冲突的产生,但一个良好的哈希函数的得来很大程度上取决于大量的实践,不过幸好前人已经总结实践了很多高效的哈希函数,可以参考大神Lucifer文章:数据结构:HashTable: http://www.cnblogs.com/lucifer1982/archive/2008/06/18/1224319.html


但是为什么一定是0.75?而不是0.8,0.6#

本着不嫌事大的精神继续深挖,在此之前先简单补充点本文需要的基础知识:

1.冲突定义:假设哈希表的地址集为[0,n),冲突是指由关键字得到的哈希地址为j(0<=j<=n-1)的位置上已经有记录。在关键字得到的哈希地址上已经有记录,那么就称之为冲突。

2.处理冲突:就是为该关键字的记录扎到另一个“空”的哈希地址。即在处理哈希地址的冲突时,若得到的另一个哈希地址H1仍然发生冲突,则再求下一个地址H2,若H2仍然冲突,再求的H3,直至Hk不发生冲突为止,则Hk为记录在表中的地址。


处理冲突的几种方法:#

一、 开放定址法#

Hi=(H(key) + di) MOD m i=1,2,...k(k<=m-1)其中H(key)为哈希函数;m为哈希表表长;di为增量序列。

开放定址法根据步长不同可以分为3种:

1)线性探查法(Linear Probing):di=1,2,3,...,m-1
  简单地说就是以当前冲突位置为起点,步长为1循环查找,直到找到一个空的位置就把元素插进去,循环完了都找不到说明容器满了。就像你去一条街上的店里吃饭,问了第一家被告知满座,然后挨着一家家去问是否有位置一样。

2)线性补偿探测法:di=Q 下一个位置满足 Hi=(H(key) + Q) mod m i=1,2,...k(k<=m-1) ,要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。
继续用上面的例子,现在你不是挨着一家家去问了,拿出计算器算了一下,然后隔Q家问一次有没有位置。

3)伪随机探测再散列:di=伪随机数序列。还是那个例子,这是完全根据心情去选一家店来问了

缺点:

  • 这种方法建立起来的hash表当冲突多的时候数据容易堆聚在一起,这时候对查找不友好;
  • 删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点
  • 当空间满了,还要建立一个溢出表来存多出来的元素。

二、再哈希法#

Hi = RHi(key),i=1,2,...k
RHi均是不同的哈希函数,即在同义词产生地址冲突时计算另一个哈希函数地址,直到不发生冲突为止。这种方法不易产生聚集,但是增加了计算时间。

缺点:增加了计算时间。

三、建立一个公共溢出区#

假设哈希函数的值域为[0,m-1],则设向量HashTable[0...m-1]为基本表,每个分量存放一个记录,另设立向量OverTable[0....v]为溢出表。所有关键字和基本表中关键字为同义词的记录,不管他们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。

简单地说就是搞个新表存冲突的元素。

四、链地址法(拉链法)#

将所有关键字为同义词的记录存储在同一线性链表中,也就是把冲突位置的元素构造成链表。

拉链法的优点:

  • 拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
  • 由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
  • 在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

拉链法的缺点:

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

Java中HashMap的数据结构#

HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

 
HashMap数据结构,来源于网络

看图就可以知道Java中的hashMap使用了拉链法处理冲突。
HashMap有一个初始容量大小,默认是16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

为了减少冲突的概率,当hashMap的数组长度到了一个临界值就会触发扩容,把所有元素rehash再放到扩容后的容器中,这是一个非常耗时的操作。

而这个临界值由【加载因子】和当前容器的容量大小来确定:DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR ,即默认情况下是16x0.75=12时,就会触发扩容操作。

所以使用hash容器时尽量预估自己的数据量来设置初始值。具体代码实现自行去研究HashMap的源码。

基础知识补充完毕,回到正题,为什么加载因子要默认是0.75?
从hashmap源码注释里找到了这一段

Ideally, under random hashCodes, the frequency of

  • nodes in bins follows a Poisson distribution
  • (http://en.wikipedia.org/wiki/Poisson_distribution) with a
  • parameter of about 0.5 on average for the default resizing
  • threshold of 0.75, although with a large variance because of
  • resizing granularity. Ignoring variance, the expected
  • occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
  • factorial(k)). The first values are:
  • 0: 0.60653066
  • 1: 0.30326533
  • 2: 0.07581633
  • 3: 0.01263606
  • 4: 0.00157952
  • 5: 0.00015795
  • 6: 0.00001316
  • 7: 0.00000094
  • 8: 0.00000006
  • more: less than 1 in ten million

注意wiki链接中的关键字:Poisson_distribution
泊淞分布啊

简单翻译一下就是在理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素个数和概率的对照表。

从上面的表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。

好了,再深挖就要挖到统计学那边去了,就此打住,重申一下使用hash容器请尽量指定初始容量,且是2的幂次方。

关于泊淞分布的知识请看 泊松分布和指数分布:10分钟教程

参考:为什么Java中的HashMap默认加载因子是0.75

HashMap默认加载因子为什么选择0.75?(阿里)的更多相关文章

  1. [转]为什么Java中的HashMap默认加载因子是0.75

    前几天在一个群里看到有人讨论hashmap中的加载因子为什么是默认0.75. HashMap源码中的加载因子 static final float DEFAULT_LOAD_FACTOR = 0.75 ...

  2. 为什么HashMap的加载因子是0.75?

    说在前面 ​ 在HashMap中,默认创建的数组长度是16,也就是哈希桶个数为16,当添加key-value的时候,会先计算出他们的哈希值(h = hash),然后用return h & (l ...

  3. 关于new HashMap<>(1)中1的理解(hashMap的加载因子)

    新入公司,阅读代码的时候发现了一行代码,为 Map<String, String> map=new HashMap<>(1); 对于这个括号里面的1不能理解,于是查了资料,大概 ...

  4. HashMap 扩容 加载因子

    HashMap: public HashMap(int initialCapacity, float loadFactor) { //初始容量不能<0 if (initialCapacity & ...

  5. 为什么HashMap初始大小为16,为什么加载因子大小为0.75,这两个值的选取有什么特点?

    先看HashMap的定义: public class HashMap<K,V>extends AbstractMap<K,V>implements Map<K,V> ...

  6. ArrayList、Vector、HashMap、HashSet的默认初始容量、加载因子、扩容增量

    当底层实现涉及到扩容时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来的数据全部复制到新的内存上,这无疑使效率大大降低. 加载因 ...

  7. ArrayList、Vector、HashMap、HashTable、HashSet的默认初始容量、加载因子、扩容增量

    这里要讨论这些常用的默认初始容量和扩容的原因是: 当底层实现涉及到扩容时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来的数据全 ...

  8. List、Map、set的加载因子,默认初始容量和扩容增量

    首先,这三个概念说下.初始大小,就是创建时可容纳的默认元素个数:加载因子,表示某个阀值,用0~1之间的小数来表示,当已有元素占比达到这个阀值后,底层将进行扩容操作:扩容方式,即指定每次扩容后的大小的规 ...

  9. Java集合类初始容量、加载因子、扩容增量

    当底层实现涉及到扩容时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来的数据全部复制到新的内存上,这无疑使效率大大降低. 加载因 ...

随机推荐

  1. java 编译时注解框架 lombok-ex

    lombok-ex lombok-ex 是一款类似于 lombok 的编译时注解框架. 编译时注,拥有运行时注解的便利性,和无任何损失的性能. 主要补充一些 lombok 没有实现,且自己会用到的常见 ...

  2. Centos 下安装 Nginx(新)

    今天重新实践了下 CentOS 7.6 下安装 Nginx,总结了一条更直接并简单的方式 从官方获取写入 nginx.repo 的方式 从官网查看文档,获取 nginx.repo 的文档内容,将其内容 ...

  3. Consul初探-集成ocelot

    前言 由于 Consul 的高可用性.丰富的API.友好的 Web 控制台界面等特点,Consul 的发展非常迅猛,得益于 .NETCore 社区的快速发展和社区成员的贡献,我们现在可以非常方便快速的 ...

  4. AndroidStudio集成.so遇见的问题:关于java.lang.UnsatisfiedLinkError: Native method not found

    我调试的是串口程序,程序中需要继承.so文件,AndroidStudio中集成.so文件的方法是将存放.so的文件夹(通常这个文件夹名字是:armeabi)拷贝到app的libs文件夹中,然后在app ...

  5. IDEA新建Spring配置文件的方法

    IDEA创建Spring Config 选择项目文件右键 输入文件名称即可 applicationContext.xml

  6. Fiddler常用设置

    1.设置抓取HTTPS请求 勾选后弹窗添加证书确认框 点击yes后,弹出警告 点击是,成功添加证书 点击OK确认,设置成功了 成功抓取到HTTPS请求 2.自定义会话框,展示GET和POST请求 3. ...

  7. JSON.parse和JSON.stringify方法

    1.JSON.parse(“要转换的数据”) 2.JSON.stringify(“要转化的数据”) 3.首先,两个方法的用法是有差别的: ①.JSON.stringify是从一个对象中解析出字符串 ② ...

  8. 3.华为路由交换技术_IP子网划分

    IP地址组成 地址包含两部分:网络号+主机号 比如:深圳市(网络号)宝安区坂田华为基地A10(主机号) 约定黄色部分为网络号. D类地址作为组播,E类地址保留使用(用于研究) 路由器只关心IP地址的网 ...

  9. 2.28秒创建一个k8s集群(非理论篇,理论自行 -- )

    准备3台centos 7+ (建议7以上,不然要会很麻烦,要升级内核等等,扯淡的东西) 安装docker 和k8s集群(均以最新版为例)基于官网 设置静态ip(可选) 查看本机的网关ip cd /Li ...

  10. 09. Go 语言并发

    Go 语言并发 并发指在同一时间内可以执行多个任务.并发编程含义比较广泛,包含多线程编程.多进程编程及分布式程序等.本章讲解的并发含义属于多线程编程. Go 语言通过编译器运行时(runtime),从 ...