HashMap中的为什么hash的长度为2的幂而&位必须为奇数
2017-11-09 23:43
232 查看
背景
哈希算法在Java中是经常用的的一个算法,也是一些常用数据结构中必用的一个算法,它为上层的复杂数据结构提供了基础支撑。哈希算法的实现有很多种,除了这里讲的map中的hashcode算法,还有其他哈希算法:
1.直接定址法
2.数字分析法
3.折叠法
4.平方取中法
5.减去法
6.字符串数值哈希法
7.旋转法
更多算法请参考另一篇作者的文章:
hash算法原理详解
HashMap中的HashCode算法详解
1,哈希算法在HashMap类中的应用java中的集合,比如HashMap/Hashtable/HashSet等,在实现时,都用到了哈希算法。当我们向容器中添加元素时,我们有必要知道
这个元素是否已经存在。
从实现上来说,java是借助hashcode()方法和equals()方法来实现判断元素是否已经存在的。当我们向HashMap中插入元素A时,首先,
调用hashcode()方法,判断元素A在容器中是否已经存在。如果A元素的hashcode值在HashMap中不存在,则直接插入。否则,接着调用
equals()方法,判断A元素在容器中是否已经存在。hashcode()的时间复杂度为O(1),equals()方法的时间复杂度为O(m),整体的时间复杂度
就是:O(1) + O(m)。其中,m是桶的深度。桶的深度是一个什么概念呢,桶的深度是指具有相同hashcode值得元素的个数,也就是发生哈希
碰撞的元素的个数。
一个好的哈希算法应该尽量减少哈希碰撞的次数。
HashCode是Object中本身就有的方法,但是没有具体实现。而各个不同的数据类型又继承实现了各自的具体HashCode算法,这里只以String类型的HashCode为例。
public int hashCode() { int h = hash; if (h == 0) { int off = offset; char val[] = value; int len = count; for (int i = 0; i < len; i++) { h = 31*h + val[off++]; } hash = h; } return h; }
源代码写的比较简洁,阅读起来也不是太方便,下面我详细解读一下:
// String类的hashcode值(哈希值)是如何计算得到的?具体实现?为了方便阅读,我们来进行分步说明
static void hashcodeTest(){ String str = "yangcq"; // 第一步 = (int)'y' // 第二步 = (31 * (int)'y') + (int)'a' // 第三步 = 31 * ((31 * (int)'y') + (int)'a') + (int)'n' // 第四步 = 31 * (31 * ((31 * (int)'y') + (int)'a') + (int)'n') + (int)'g' // 第五步 = 31 * (31 * (31 * ((31 * (int)'y') + (int)'a') + (int)'n') + (int)'g') + (int)'c' // 第六步 = 31 * (31 * (31 * (31 * ((31 * (int)'y') + (int)'a') + (int)'n') + (int)'g') + (int)'c') + (int)'q' // 上面的过程,也可以用下面的方式表示 // 第一步 = (int)'y' // 第二步 = 31 * (第一步的计算结果) + (int)'a' // 第三步 = 31 * (第二步的计算结果) + (int)'n' // 第四步 = 31 * (第三步的计算结果) + (int)'g' // 第五步 = 31 * (第四步的计算结果) + (int)'c' // 第六步 = 31 * (第五步的计算结果) + (int)'q' int hashcode = 31 * (31 * (31 * (31 * ((31 * (int)'y') + (int)'a') + (int)'n') + (int)'g') + (int)'c') + (int)'q'; System.out.println("yangcq的hashcode = " + hashcode); // yangcq的hashcode = -737879313 System.out.println("yangcq的hashcode = " + str.hashCode()); // yangcq的hashcode = -737879313 }
为什么HashMap中的&位必须为奇数(Length - 1)
从Key映射到HashMap数组的对应位置,会用到一个Hash函数:index = Hash(“apple”)
如何实现一个尽量均匀分布的Hash函数呢?我们通过利用Key的HashCode值来做某种运算。
如何进行位运算呢?有如下的公式(Length是HashMap的长度):
index = HashCode(Key) & (Length - 1)
下面我们以“book”的Key来演示整个过程:
1.计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。
2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。
3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。
可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。
假设HashMap的长度是10,重复刚才的运算步骤:
单独看这个结果,表面上并没有问题。我们再来尝试一个新的HashCode 101110001110101110 1011 :
让我们再换一个HashCode 101110001110101110 1111 试试 :
是的,虽然HashCode的倒数第二第三位从0变成了1,但是运算的结果都是1001。也就是说,当HashMap长度为10的时候,有些index结果的出现几率会更大,而有些index结果永远不会出现(比如0111)!
这样,显然不符合Hash算法均匀分布的原则。
反观长度16或者其他2的幂,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。
相关文章推荐
- 为什么数组可以不加“&”取它的地址,而普通变量必须要加“& ”才可以呢?
- 针对“【ASP.NET】"密码最短长度为7,其中必须包含以下非字母数字字符1"解决方法
- 密码最短长度为7,其中必须包含以下非字母数字字符: 1"
- linux kill(int pid,int signo) 其中kill 要杀死pid ,为什么必须要signo > 0 ,signo 发给谁,谁来接收,有什么用处
- "密码最短长度为7,其中必须包含以下非字母数字字符1"解决方法
- HashMap中(tab.length - 1) & hash的神奇之处
- HashMap & HashSet & Hashtable
- 什么是 哈希表 HashMap 中数组的 size 为什么必须是 2 的整数次幂
- 为什么operator>>(istream&, string&)能够安全地读入长度未知的字符串?
- 为什么坐标变换的顺序必须是: 缩放->旋转->平移
- HashMap的默认长度为什么是16?
- 为什么operator>>(istream&, string&)能够安全地读入长度未知的字符串?
- "密码最短长度为7,其中必须包含以下非字母数字字符1"解决方法
- LinkedHashMap & LinkedHashSet
- 为什么operator>>(istream&, string&)能够安全地读入长度未知的字符串?
- ConcurrentHashMap中并发级别,桶内hash表的长度,扩容门槛的确定
- HashMap为什么线程不安全(hash碰撞与扩容导致)
- HashMap中entry.hash == hash && (k == key || key.equals(k))
- HashMap的长度为什么要是2的n次方
- 为什么Java中的HashMap<K, V>的get函数是get(Object key),而不是get(K key)?