Hashmap的学习

一:hash 算法

hash 值的计算,目的为了找到在数组中的index下标。

数组下标index = hash & (n-1) // n 代表的是数组的长度,hashmap 里数组的初始长度是16

 上面源码的写法,等价于下面的,如下:

hash>>>16: 表示高位的,移动到了低位,低位的全部消失,然后高位全部补0

问题一:为啥要让高位的参与进来呢?

如果高位不参与进来,那么参与计算的只有低4位了,因为是 & 运算,而且(16-1)用二进制表示是 15 = 1111,  所以取决第四位 与 1111 进行 &运算 ,这样低四位全为0,结果位0,低四位全为1,结果位1111,所以取值范围是0000~1111.

问题二:为什么是 异或运算呢?

从这个图可以看出:

1, & 运算 ,偏向0的概率为 0.75

2., | 运算,偏向1 的 概率为 0.75

3,^ 异或运算,0和1的概率都是0.5,这样更均衡、

hash 算法,主要是数组下标定位,这样改进的目的,都是为了,避免hash 冲突。

二: hash冲突了,怎么解决?

解决hash冲突(哈希冲突)有以下四种方法:

链地址法
再哈希法
建立公共溢出区
开放定址法


方法1:链地址法
对于相同的哈希值,使用链表进行连接。(HashMap使用此法)

优点

处理冲突简单,无堆积现象。即非同义词决不会发生冲突,因此平均查找长度较短;
适合总数经常变化的情况。(因为拉链法中各链表上的结点空间是动态申请的)
占空间小。装填因子可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计
删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。


缺点

查询时效率较低。(存储是动态的,查询时跳转需要更多的时间)
在key-value可以预知,以及没有后续增改操作时候,开放定址法性能优于链地址法。
不容易序列化


方法2:再哈希法
提供多个哈希函数,如果第一个哈希函数计算出来的key的哈希值冲突了,则使用第二个哈希函数计算key的哈希值。

优点

不易产生聚集


缺点

增加了计算时间


方法3:建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

法4:开放定址法
当关键字key的哈希地址p =H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,若p1仍然冲突,再以p1为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。

即:Hi=(H(key)+di)% m (i=1,2,…,n)

开放定址法有下边三种方式:

线性探测再散列


1. 顺序查看下一个单元,直到找出一个空单元或查遍全表
2. di=1,2,3,…,m-1


二次(平方)探测再散列


1. 在表的左右进行跳跃式探测,直到找出一个空单元或查遍全表
2. di=1^2,-1^2,2^2,-2^2,…,k^2,-k^2 ( k<=m/2 )


伪随机探测再散列


建立一个伪随机数发生器,并给一个随机数作为起点
di=伪随机数序列。具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。

例如,已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。

如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元。

如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元。

如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,……..,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元。

优点

容易序列化
若可预知数据总数,可以创建完美哈希数列


缺点

1. 占空间很大。(开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间)
2. 删除节点很麻烦。不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。
 

三:hashmap 的put 源码解读

成员 变量一:数组初始容量 = 16 (1<< 4, 表示1 左移4位,即10000)

成员变量二:扩容因子 = 0.75

为什么是0.75呢?

1     空间利用率很高,但是提高了查询成本(因为1的话,表示数组已经全部使用,再添加元素的话,就会有冲突,无论放哪个index 都有元素,这样会形成链表,链表的查询效率低)

0.5   空间利用低,

0.75 是 空间和时间的折中。

成员变量三:树化参数 8

为什么链表的长度是8,就会转化成树呢?

 从源码中可以看到, hash 值出现8次一样的值时,即有8次冲突的概率是非常低的。

成员变量四:树化的最小数组容量 = 64 (16 扩容一次变为32, 32再扩容一次变为64,即要两次扩容才到达了转化为树的条件)

hashmap  put 源码解读

流程图:

 

// 第一步:数组是否为空的判断, 是——> 初始化数组

// 第二步:判断数组下标为i的位置,元素是否为空, 是——> 直接new一个节点/元素

// 第三步:判断下标i 位置的key, 和要插入的元素的key 是否相等,

是 ——>   值的覆盖;

不是——>判断next 节点是否为空,

为空,在节点后面 插入/new 一个节点/元素

不为空,e再赋值给p,继续遍历p的下一个节点,是否为空

// 第四步:节点next 的指向是否为空,即有没有下一个节点, 是——> 新的节点直接插在后面

// 第五步:是否要树化, 链表的长度是否为8,即遍历了8次,是——> 执行treeifyBin(tab, hash);

 

 初始化数组或者扩容的方法,如下:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值