二进制基础回顾

以下操作相对正整数的二进制而言,对非整数不太适用。

二进制转十进制



  在二进制中,位权是2的幂,所以每一位所代表的权值从右到左分别为2^(1-1) 、2^(2-1) 、... 、 2^(n-1) ,第n位的权值为2的(n-1)次幂。

所以: 100101 = 2^5 + 2^2 + 2^0 = 37。

二进制位移操作



  当一个二进制数左移一位,右补"0"的时候,这个数每一位的权值就变成了原来的两倍,那么整个数值也扩大了2倍;当这个数左移n位的时候,这个数就扩大到原来的2^n 倍。同样的,往右移动n位,左补"0",相当于除以2^n ,如果考虑到右边数位被舍弃的问题,这就相当于除以2^n 然后取整数。这和十进制是一样的,十进制数字左移n位,就是扩大到原来的10^n 倍……

“按位与”与取模

在HashMap和ThreadLocal源码中可以看到类似这样的操作:





  在这里,“按位与”操作的作用是取模,其中"n"和"len"都是数组的长度,此处代码是要把元素的hash值映射成数组的索引(下标),以此来决定该元素的存储位置。由于hash值相对于数组的长度来说很大,所以不能把hash值直接一一映射为数组下标,而是对其取模,通过余数来映射。关于详细的取模的意义,详见百度-散列表。

原理

当n等于2的次幂时,"m%n"和"m&(n-1)"等价,求证如下:

设n=16,hash=2740216402

  • 当n取2的幂时,n的二进制表示有个特点——除去左边补全的0外,数字以"1"开头,后面全是"0";n-1的二进制表示也有一个特点——n-1的二进制位数比n少一位,数位左边全是"0",右边全是"1"。

  • n-1与hash值进行“按位与”操作时,就相当于把hash前面部分舍去,只保留后面部分(这与掩码类似,实际上在源码注释部分,也把这操作称为"mask")。这实际上就是取模操作,后面的保留部分棕红色的0010就是“余数”。为什么这部分是余数?接着往下求证:

  把hash值2740216402的二进制表示拆成两部分,可变为:



解析: r为保留部分,hash=p+r

  p与n是存在倍数关系的,如下所示:



  总结上述数量关系,可得:"hash = p + r = q * n + r",又由于r比n小,所以r自然就是"hash % n"的余数,因此当n等于2的幂,hash&(n-1)=hash%n。

HashMap中的异或操作

在HashMap源码中有这样一段代码:

/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

先简单了解下java的位移操作:

  • <<:左移运算符,num << 1,相当于num乘以2
  • >>:右移运算符,num >> 1,相当于num除以2
  • >>>:无符号右移,忽略符号位,空位都以0补齐

关于异或的知识:

  • A ^ 0 = A,即当0与一个数(0/1)进行异或操作时,结果等于这个数本身,如0 ^ 0 = 0、 0 ^ 1 = 1;
  • A ^ 1 = ! A,即当1与一个数(0/1)进行异或操作,结果等于非-此数,或者说取反,如1 ^ 0 = 1, 1 ^ 1 = 0;

再来画下图看这里代码干了些什么:



  好了,图已经画出来了,接下来对该操作进行解读——代码先是这样……然后那样……最后又……

  解读个鬼啊,其实这样很难看出这段操作的意义,所以还是看代码上面的注释吧,注释写得很清楚了,这段代码主要作用是利用hashcode的结果来生成更加“散列”的哈希值hash,什么意思?接着往下看:

  接着上一小节的“按位与”讲起,思考下,如果用原始的"hashcode"执行上小节的“按位与操作”会与怎样的问题。



  引用上面的旧图分析,如果直接用原始的hashcode来取模,然后映射为数组的下标,这样会产生一个很大的问题。通常数组的长度不会太大,即上图红棕色的部分不会很长,那么原始的hashcode的“高位”对最后的余数的影响会很小,意思就是,只要hashcode后面的四位数为"0010",不管前面蓝紫色部分是什么,“hashcode&(n-1)”的结果始终为"0010",映射为数组的下标就是“2”,这样会非常容易造成“哈希冲突”(又名“哈希碰撞”)。

  所以需要采取一种策略,使得hashcode的每一位,都尽量参与运算,尽量对取模结果产生影响,充分利用hashcode的每一位,使得取模的结果更加“零散”。因此,HashMap的源码给出了以上的方法。

  hashcode长度为32位,右移16位,就是给原始的hashcode“折成两半”,把高位的一半与低位的一半对齐,然后通过异或操作把高位和低位“结合”起来。



  生成的新的hash值,其高位部分(左边16位蓝紫色部分)保留了原hashcode的高位,低位部分(红色部分)保留了原来的高位和低位的“特征”——如果原来高位部分某一位发生改变,则影响到结果的对应位;如果原来低位某一位发生改变,也同样影响到结果相应的位。

  这里有一个问题,为什么要用异或操作?因为只能用异或操作,因为“与”和“或”不能很好的保留操作数的特征:

  • 使用“与”操作时,当一个数为“0”,则结果必然为“0”,不必考虑另一个操作数;
  • 使用“或”操作时,当一个操作数为“1”,则结果必然为“1”;
  • 使用“异或”操作时,需要知道两个操作数才能决定结果。

  当用上述方法生成新的hash值后,原来的hashcode的每一位都对最终的取模结果产生了影响,这时在一定程度上可以使得生成的余数更加均匀,更加“散列”,使得发生“碰撞”的几率降低。

HashMap中的位运算的更多相关文章

  1. PHP中的位运算与位移运算(其它语言通用)

    /* PHP中的位运算与位移运算 ======================= 二进制Binary:0,1 逢二进1,易于电子信号的传输 原码.反码.补码 二进制最高位是符号位:0为正数,1为负数( ...

  2. C语言中的位运算和逻辑运算

    这篇文章来自:http://blog.csdn.net/qp120291570/article/details/8708286 位运算 C语言中的位运算包括与(&),或(|),亦或(^),非( ...

  3. Google Earth Engine 中的位运算

    Google Earth Engine中的位运算 按位运算是编程中一个难点,同时也是在我们后续处理影像数据,尤其要使用影像自带的波段比如QA波段经常会用到的一个东西.通过按位运算我们可以筛选出我们想要 ...

  4. js中的位运算

    按位运算符是把操作数看作一系列单独的位,而不是一个数字值.所以在这之前,不得不提到什么是"位": 数值或字符在内存内都是被存储为0和 1的序列,每个0和1被称之为1个位,比如说10 ...

  5. C#学习笔记-----C#枚举中的位运算权限分配

    一.基础知识 什么是位运算? 用二进制来计算,1&2:这就是位运算,其实它是将0001与0010做位预算   得到的结果是 0011,也就是3  2.位预算有多少种?(我们就将几种我们权限中会 ...

  6. C++中的位运算总结

    1)位运算 位运算是指对转换成二进制的数字进行每一位上的0.1的运算,运算涉及到五种运算:与(&),或(|),异或(^),左移(<<),右移(>>). 如下表所示:   ...

  7. 关于C/C++中的位运算技巧

    本篇文章讲述在学习CSAPP位运算LAB时的一些心得. 移位运算的小技巧 C/C++对于移位运算具有不同的策略,对于无符号数,左右移位为逻辑移位,也就是直接移位:对于有符号数,采用算术移位的方式,即左 ...

  8. C语言中的位运算的技巧

    一.位运算实例 1.用一个表达式,判断一个数X是否是2的N次方(2,4,8,16.....),不可用循环语句. X:2,4,8,16转化成二进制是10,100,1000,10000.如果减1则变成01 ...

  9. C/C++中的位运算

    位运算     位运算的运算分量只能是整型或字符型数据,位运算把运算对象看作是由二进位组成的位串信息,按位完成指定的运算,得到位串信息的结果. 位运算符有:     &(按位与).|(按位或) ...

随机推荐

  1. uva 11806 Cheerleaders (容斥)

    http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&p ...

  2. @noi.ac - 488@ cleaner

    目录 @description@ @solution@ @accepted code@ @details@ @description@ 小Q计划在自己的新家中购置一台圆形的扫地机器人.小Q的家中有一个 ...

  3. @游记@ CQOI2019(十二省联考)

    目录 @day - 0@ @day - 1@ @day - 2@ @后记@ 我只是来打酱油哒-- 顶多能进个 E 类继续打酱油. 原本还在互奶 A 队,结果现在--铁定进不了队啦. 对初中生的歧视啊 ...

  4. springboot&mybatis 增删改查系列(一)

    创建父项目 首先,我们需要创建一个Maven项目. 在这个项目的pom文件中加入以下几个依赖: <!-- spring boot --> <parent> <groupI ...

  5. Python--day66--内容回顾

    3,python中的大小比较和js中的大小比较规则: python中a>b>c,就是先比较a>b,然后再比较b>c,都为true的话就返回true: js中的a>b> ...

  6. Python--day27--几个内置方法:__repr__()/__str__()/__del__()/__call__()/__getitem__/__setitem/delitem/__new__/__eq__/__hash__

    repr方法() 双下方法__str__: 打印对象就相当于打印对象.__str__ __repr__(): __repr__是__str__的备胎,没有__str__的时候,就调用__repr__: ...

  7. Python--day42--mysql创建用户及授权

    1,打开mysql数据库命令:mysql -u root -p 2,创建数据库create database db2; 2,删除数据库drop database db2; 2,展示有多少个文件夹:sh ...

  8. Junit测试代码时出现initializationError 错误

    首先代码没有错误,执行Junit测试时出现以上错误.上网查资料发现少了包 从网上下载了一个jar包解决了hamcrest-core-1.3.jar 现在下载包搜索的好多坑,有的网站必须注册才能下载,而 ...

  9. js中的克隆

    1.如果克隆对象是基本类型,直接复制就可以 <script type="text/javascript"> var str1 = 'abc' var str2 = st ...

  10. 牛客2018国庆集训 DAY1 D Love Live!(01字典树+启发式合并)

    牛客2018国庆集训 DAY1 D Love Live!(01字典树+启发式合并) 题意:给你一颗树,要求找出简单路径上最大权值为1~n每个边权对应的最大异或和 题解: 根据异或的性质我们可以得到 \ ...