论坛首页 Java企业应用论坛

深入理解HashMap

浏览 130385 次
该帖已经被评为精华帖
作者 正文
   发表时间: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源代码,失误了。
0 请登录后投票
   发表时间:2009-12-03   最后修改:2009-12-03
zhang_xzhi_xjtu 写道

请从理论上阐明2的幂作为size效率是最高的。
HashMap是用的&,HashTable用的是%,这里我的意思是说求模是算法,而如何求是实现。不知道??是问什么?

如果不是2的幂,那么势必会有一些位置一直都是空的,你的误解应该在于“效率高”这三个字是针对什么的,显然楼主是针对hashmap,并不是针对&操作,不是2的幂,那么hashmap中碰撞几率高,那么get的时候效率就低。


??是为这句(不过你在上面一贴已经作了说明了):
引用
如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。

0 请登录后投票
   发表时间:2009-12-03  
火星来客 写道
zhang_xzhi_xjtu 写道

请从理论上阐明2的幂作为size效率是最高的。
HashMap是用的&,HashTable用的是%,这里我的意思是说求模是算法,而如何求是实现。不知道??是问什么?

如果不是2的幂,那么势必会有一些位置一直都是空的,你的误解应该在于“效率高”这三个字是针对什么的,显然楼主是针对hashmap,并不是针对&操作,不是2的幂,那么hashmap中碰撞几率高,那么get的时候效率就低。


??是为这句(不过你在上面一贴已经作了说明了):
引用
如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。



这句话我认为不对,如果不是2次幂的话,就不会用&了,而是用%。真正的目的就是要让元素均匀的分布在数组中,&只是在数组长度是2次幂的情况下的一种快速求模的方法。
0 请登录后投票
   发表时间: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)。


0 请登录后投票
   发表时间:2009-12-03  
kimmking 写道
这都精华了,
哎,我写了个介绍各种常见数据结构(ArrayList Hashmap/table。。。)的jdk实现和.net实现的原理分析的,
发到 算法和数据结构版本,被新手帖了。。。

哎,以后写啥都发java版。

你的关于hashmap文章在哪里,拜读一下,因为我有可能需要更正我在4楼说的话。
0 请登录后投票
   发表时间:2009-12-03   最后修改:2009-12-03
zhang_xzhi_xjtu 写道

如果只是从&和%的角度看2的幂作为size效率是最高的,我同意。

如果不是2的幂,那么势必会有一些位置一直都是空的。这个我是不同意的,原因我说了,这是因果倒置。
因为java选择2的幂作为size,所以用&进行了code级别的优化。
如果不选择2的幂,则没有这个代码级别的优化,而会用%这个较慢的操作,但是这里是不会有势必会有一些位置一直都是空的这个问题的。
换言之,选择2的幂作size是&的前条件,如果不是2的幂的话,&就不成立了。(当然,还是一个hash,不过是一个很烂的hash)。



ok,那么原理上我们没有分歧,主要是鸡蛋的问题,鸡蛋问题就不说了
0 请登录后投票
   发表时间:2009-12-03  
火星来客 写道
dennis_zane 写道

h&length-1,这个也算是常识吧,对数学或者位运算了解点就知道了。

&运算本身当然是常识,任何一本计算机图书中几乎都会提到,但是将其和:
int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

组合起来使用,得到%的效果,但是性能比%高,这一点我没有想到,所以这一点上对我个人来说没有将其视为常识。


组合起来使用,得到%的效果?
0 请登录后投票
   发表时间: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的为幂的散列效果要好。
0 请登录后投票
   发表时间:2009-12-03  
回复的时候没注意,上升到鸡蛋问题了,gameover?
0 请登录后投票
   发表时间:2009-12-03   最后修改:2009-12-03
shelocks 写道
回复的时候没注意,上升到鸡蛋问题了,gameover?

但是由于你下面这个回复,所以这个gameover的游戏可以被重新启动,等别人去解释吧,呵呵。
shelocks 写道

  Java中采用2的幂作为容量大小的优势在于,散列函数的效率比较高,与操作肯定比mod要快。并不意味着以2的为幂的散列效果要好。

0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics