hash函数为什么要选择对素数求余?

hash函数为什么要选择对素数求余?

问题来源?

  引出此问题,是看到一篇有关jdk中HashMap和Hashtable对于hash算法的选择。

  1. HashMap中对key求完hash值,在进行数组寻址时,使用的方法是位运算(代替的取模运算)。公式如下:
    (length - 1) & hash  // length为HashMap的容量,是2的n次方

  在这里插播一个小知识点:位运算(&)比模运算(%)效率高很多,原因是位运算直接对内存数据进行操作,不需要像模运算一样转成十进制,因此处理速度快。

    // 可以使用位运算代替模运算的原因,见以下公式:
    hash % 2^n = hash & (2^n -1)
    
    // 5 % 8 = 5 & 7 = 0110 & 0111 = 0110 = 5
    // 13 % 8 = 13 & 7 = 1110 & 0111 = 0110 =5
  1. Hashtable中求完hash值,在进行数组寻址时,使用的取模运算。公式如下:
    int index = (hash & 0x7FFFFFFF) % tab.length;
    // 此处hash和0x7FFFFFFF的一次位与操作,是为了保证得到的index值首位为0(代表正数),其实就是在取绝对值。以避免负数计算index的复杂度
    // tab.length为Hashtable的长度。默认初始化为11,之后rehash每次扩容为oldCapacity * 2 + 1

  前面说过,HashMap之所以不用取模的原因是为了提高效率,为什么Hashtable还要使用?有人认为,因为HashTable是个线程安全的类,本来就慢,所以Java并没有考虑效率问题,就直接使用取模算法了呢?但是其实并不完全是,Java这样设计还是有一定的考虑在的,虽然这样效率确实是会比HashMap慢一些。
  HashTable简单的取模是有一定的考虑在的。这就要涉及到HashTable的构造函数和扩容函数。Hashtable的长度:默认初始化为11,之后rehash每次扩容为oldCapacity * 2 + 1。也就是说,HashTable的链表数组的默认大小是一个素数、奇数。之后的每次扩充结果也都是奇数。。
  由于HashTable会尽量使用素数、奇数作为容量的大小。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。这就是文章开头所提到的,问题来源

那为何hash要对素数取余呢?

  常用的hash函数是选一个数m取模(余数),这个数在课本中推荐m是素数,但是经常见到选择m=2n,因为对2n求余数更快,并认为在key分布均匀的情况下,key%m也是在[0,m-1]区间均匀分布的。但实际上,key%m的分布同m是有关的。
  证明如下: key%m = key - xm,即key减掉m的某个倍数x,剩下比m小的部分就是key除以m的余数。显然,x等于key/m的整数部分,以floor(key/m)表示。假设key和m有公约数g,即key=ag, m=bg, 则 key - xm = key - floor(key/m)m = key - floor(a/b)m。由于0 <= a/b <= a,所以floor(a/b)只有a+1中取值可能,从而推导出key%m也只有a+1中取值可能。a+1个球放在m个盒子里面,显然不可能做到均匀。
  由此可知,一组均匀分布的key,其中同m公约数为1的那部分,余数后在[0,m-1]上还是均匀分布的,但同m公约数不为1的那部分,余数在[0, m-1]上就不是均匀分布的了。把m选为素数,正是为了让所有key同m的公约数都为1,从而保证余数的均匀分布,降低冲突率。
  备注:floor函数为:向下取整

你可能感兴趣的:(Java基础相关)