锁定老帖子 主题:深入理解HashMap
该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2009-12-03
dennis_zane 写道 zhang_xzhi_xjtu 写道 虽然精华了,但是还是要挑挑刺。
annegu 写道 看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率! 所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。这就是真正得原因,很多人即使看过源代码也不知道原因。 说道÷到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。 所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):[/size] // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; 这一段的描述是有些容易误导读者,主要原因是因为搞混了因果关系。 java的hash实现用的是除法求模的方式,是设计一个hash算法最简单的方式之一。 而return h & (length-1);只不过是当length为2的幂的时候的一种求模的代码级的优化而已。为了速度。 如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。 就hash的算法而言,size是不是2的幂是不重要的,并不会出现lz说的浪费空间问题。这个是由算法决定的。 就java的实现而言,以2的幂为size的大小,是可以得到一些速度上的优势,但是也是有它自己的缺陷的。 那就是hash值的高位没有参与到hash计算中。 如 size=0x100,则对0xab11,0xac11,0xee11的hash计算结果是一样的,都为0x11。在一些特殊的输入,如高位变化,低位不变的情况下,hash冲突很严重。其原因就是只有低位参与了hash计算。 由于2在计算机程序的特殊性,以上也是一个要考虑的东西,所以大部分的算法书在介绍除法模式的hash时,都是建议用质数的。 事实上,HashMap考虑了你说到的这种情况,对hash值做了处理,加入了高位的计算: /** * Applies a supplemental hash function to a given hashCode, which * defends against poor quality hash functions. This is critical * because HashMap uses power-of-two length hash tables, that * otherwise encounter collisions for hashCodes that do not differ * in lower bits. Note: Null keys always map to hash 0, thus index 0. */ static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } 这个是我按照楼主的思路去写的,没有检查java源代码,失误了。 |
|
返回顶楼 | |
发表时间:2009-12-03
最后修改:2009-12-03
zhang_xzhi_xjtu 写道 请从理论上阐明2的幂作为size效率是最高的。 HashMap是用的&,HashTable用的是%,这里我的意思是说求模是算法,而如何求是实现。不知道??是问什么? 如果不是2的幂,那么势必会有一些位置一直都是空的,你的误解应该在于“效率高”这三个字是针对什么的,显然楼主是针对hashmap,并不是针对&操作,不是2的幂,那么hashmap中碰撞几率高,那么get的时候效率就低。 ??是为这句(不过你在上面一贴已经作了说明了): 引用 如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。
|
|
返回顶楼 | |
发表时间:2009-12-03
火星来客 写道 zhang_xzhi_xjtu 写道 请从理论上阐明2的幂作为size效率是最高的。 HashMap是用的&,HashTable用的是%,这里我的意思是说求模是算法,而如何求是实现。不知道??是问什么? 如果不是2的幂,那么势必会有一些位置一直都是空的,你的误解应该在于“效率高”这三个字是针对什么的,显然楼主是针对hashmap,并不是针对&操作,不是2的幂,那么hashmap中碰撞几率高,那么get的时候效率就低。 ??是为这句(不过你在上面一贴已经作了说明了): 引用 如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。
这句话我认为不对,如果不是2次幂的话,就不会用&了,而是用%。真正的目的就是要让元素均匀的分布在数组中,&只是在数组长度是2次幂的情况下的一种快速求模的方法。 |
|
返回顶楼 | |
发表时间:2009-12-03
火星来客 写道 zhang_xzhi_xjtu 写道 请从理论上阐明2的幂作为size效率是最高的。 HashMap是用的&,HashTable用的是%,这里我的意思是说求模是算法,而如何求是实现。不知道??是问什么? 如果不是2的幂,那么势必会有一些位置一直都是空的,你的误解应该在于“效率高”这三个字是针对什么的,显然楼主是针对hashmap,并不是针对&操作,不是2的幂,那么hashmap中碰撞几率高,那么get的时候效率就低。 ??是为这句(不过你在上面一贴已经作了说明了): 引用 如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。
如果只是从&和%的角度看2的幂作为size效率是最高的,我同意。 如果不是2的幂,那么势必会有一些位置一直都是空的。这个我是不同意的,原因我说了,这是因果倒置。 因为java选择2的幂作为size,所以用&进行了code级别的优化。 如果不选择2的幂,则没有这个代码级别的优化,而会用%这个较慢的操作,但是这里是不会有势必会有一些位置一直都是空的这个问题的。 换言之,选择2的幂作size是&的前条件,如果不是2的幂的话,&就不成立了。(当然,还是一个hash,不过是一个很烂的hash)。 |
|
返回顶楼 | |
发表时间:2009-12-03
kimmking 写道 这都精华了,
哎,我写了个介绍各种常见数据结构(ArrayList Hashmap/table。。。)的jdk实现和.net实现的原理分析的, 发到 算法和数据结构版本,被新手帖了。。。 哎,以后写啥都发java版。 你的关于hashmap文章在哪里,拜读一下,因为我有可能需要更正我在4楼说的话。 |
|
返回顶楼 | |
发表时间:2009-12-03
最后修改:2009-12-03
zhang_xzhi_xjtu 写道 如果只是从&和%的角度看2的幂作为size效率是最高的,我同意。 如果不是2的幂,那么势必会有一些位置一直都是空的。这个我是不同意的,原因我说了,这是因果倒置。 因为java选择2的幂作为size,所以用&进行了code级别的优化。 如果不选择2的幂,则没有这个代码级别的优化,而会用%这个较慢的操作,但是这里是不会有势必会有一些位置一直都是空的这个问题的。 换言之,选择2的幂作size是&的前条件,如果不是2的幂的话,&就不成立了。(当然,还是一个hash,不过是一个很烂的hash)。 ok,那么原理上我们没有分歧,主要是鸡蛋的问题,鸡蛋问题就不说了 |
|
返回顶楼 | |
发表时间:2009-12-03
火星来客 写道 dennis_zane 写道 h&length-1,这个也算是常识吧,对数学或者位运算了解点就知道了。 &运算本身当然是常识,任何一本计算机图书中几乎都会提到,但是将其和: int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; 组合起来使用,得到%的效果,但是性能比%高,这一点我没有想到,所以这一点上对我个人来说没有将其视为常识。 组合起来使用,得到%的效果? |
|
返回顶楼 | |
发表时间:2009-12-03
zhang_xzhi_xjtu 写道 火星来客 写道 zhang_xzhi_xjtu 写道 请从理论上阐明2的幂作为size效率是最高的。 HashMap是用的&,HashTable用的是%,这里我的意思是说求模是算法,而如何求是实现。不知道??是问什么? 如果不是2的幂,那么势必会有一些位置一直都是空的,你的误解应该在于“效率高”这三个字是针对什么的,显然楼主是针对hashmap,并不是针对&操作,不是2的幂,那么hashmap中碰撞几率高,那么get的时候效率就低。 ??是为这句(不过你在上面一贴已经作了说明了): 引用 如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。
如果只是从&和%的角度看2的幂作为size效率是最高的,我同意。 如果不是2的幂,那么势必会有一些位置一直都是空的。这个我是不同意的,原因我说了,这是因果倒置。 因为java选择2的幂作为size,所以用&进行了code级别的优化。 如果不选择2的幂,则没有这个代码级别的优化,而会用%这个较慢的操作,但是这里是不会有势必会有一些位置一直都是空的这个问题的。 换言之,选择2的幂作size是&的前条件,如果不是2的幂的话,&就不成立了。(当然,还是一个hash,不过是一个很烂的hash)。 我同意zhang_xzhi_xjtu的观点。由于Java采用2的幂作为容量大小,所以才有与操作求索引的结论存在(与mod是一个效果),如果不采用2的幂做容量大小,与操作求索引也就不成立了,也就不会出现楼主所说的空间浪费(依旧是用与操作话)。 至于容量大小的选择,当采用求余散列时,原则是与2的幂不太接近的质数,如果采用2的幂(p次幂)做容量的话,k mod m求余的结果是k的p个最低位数,所以hashmap中又对最初的hashcode进行了进一步的增强。 Java中采用2的幂作为容量大小的优势在于,散列函数的效率比较高,与操作肯定比mod要快。并不意味着以2的为幂的散列效果要好。 |
|
返回顶楼 | |
发表时间:2009-12-03
回复的时候没注意,上升到鸡蛋问题了,gameover?
|
|
返回顶楼 | |
发表时间:2009-12-03
最后修改:2009-12-03
shelocks 写道 回复的时候没注意,上升到鸡蛋问题了,gameover?
但是由于你下面这个回复,所以这个gameover的游戏可以被重新启动,等别人去解释吧,呵呵。 shelocks 写道 Java中采用2的幂作为容量大小的优势在于,散列函数的效率比较高,与操作肯定比mod要快。并不意味着以2的为幂的散列效果要好。 |
|
返回顶楼 | |