初衷
曾经学习hashmap路上遇到这样那样的问题,这次记录一下hashmap里面的一些位运算
1、hashmap
HashMap的底层就是通过数组+链表+红黑树的方式实现的哈希表结构。数组结构可以在O(1)的时间复杂度定位元素在数组中的位置,而位置是通过key的哈希值和数组长度取模计算出来的,而哈希值是可能相同的,也就是哈希冲突,当然,哈希值不同的时候,通过取模计算也可能产生哈希冲突,所以相同索引下的键的哈希值是可能不相同的或者说,绝大多数是不相同的。当哈希冲突时,就用到链表或者红黑树来解决,当多个元素产生哈希冲突时,这些元素都映射到数组的同一个位置,通过链表或者红黑树结构把这些元素连接起来
算法导论
在算法导论中如何设计hash函数:除法散列、乘法散列、全域散列。当然这些前提是key为自然数。如果key不是自然数,应设法转换为自然数。
- 除法散列
散列函数:h(k) = k mod m,其中k为key值,m为槽数。
注意事项:其中一种情况是,m应为与2的整数幂不太接近的质数。否则等于取k的低位,没有随机效果。
- 乘法散列
散列函数:h(k) = floor(m(kA mod 1)),其中0<A<1。
注意事项:优点是对m没特别要求,通常是2的某个幂次,因为适合计算机运算。对A没特别要求,有研究认为最好的A是(sqrt(5)-1)/2 = 0.618…。
- 全域散列
散列函数:在计算时从一组hash函数中随机的选择一个函数
散列函数样例:ha,b(k) = ((ak + b)mod p) mod m,其中p为大于最大k的质数,0<a<p,0<=b<p,a,b属于N。
注意事项:对于m没有特别要求。散列函数簇就是所有符合条件的a与b不同组合得到的函数。
在hashmap使用的是除法散列算法,好奇的老铁不禁就有这样的疑问了:根据算法导论说的除法散列法的函数h(k) = k mod m,其中k为key值,m为槽数。m应为与2的整数幂不太接近的质数,但是现在hashmap的槽数为2的幂次方,难道是设计问题?
![](https://i-blog.csdnimg.cn/blog_migrate/e02e2c50d775ff2895abfb94035e025d.png)
我来说一下我的想法:hashmap的槽数设计为2的幂次方个槽数,初始为16.所有key为null存储在0号槽位置,其实剩余的槽位就是2的幂次方-1。2的幂次方-1就是一个接近质数的数.那么就是和算法导论说法一致了。
- hash 函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 其实就是一个整数的32位二进制
old 00000111010101000001111010101010
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
当key为null的时候hash函数返回的是0h >>> 16
old 00000111010101000001111010101010
>>>
new 00000000000000000000011101010100(h = key.hashCode()) ^ (h >>> 16)
old 00000111010101000001111010101010
^
new 00000000000000000000011101010100
end 00000000000000000001111111111110最后的结果就是得到一个高16位和低16位混合的二进制
- 计算hash在数组中对应的槽位置
i = (n - 1) & hash
当hash值为0的时候数据始终存储的第一个槽位
// 运算
hash 00000000000000000001111111111110
&
n-1 00000000000000000000000000000111
end 00000000000000000000000000000110
// 这个 00000000000000000000000000000110就是hash槽的位置
学习hashmap路上遇到这样的问题不知道你有没有遇到过和我(本人位运算不是太好)一样的问题:每次在看源码的时候往往看到很多的位运算,都会花比较多的时间去对整数进行二进制转换然后再慢慢的头脑风暴进行或与非的运算比如hashmap里面的扩容为原容器的2倍,是长这样的:
// 前置忽略
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
// 后置忽略
// 其实就是一个二进制进行向左移动1位
old 0000000000000010000
new 0000000000000100000
分享两个整数转二进制字符串的方法
- 第一种
// 通过Integer.numberOfLeadingZeros查询左边0的个数
public static String toBinaryString(int num) {
if (num == 0) return ""+0;
String result = "";
int n = Integer.numberOfLeadingZeros(num);
num <<= n;
for (int i=0; i<32-n; ++i) {
int x = (Integer.numberOfLeadingZeros(num) == 0)?1:0;
result += x;
num <<= 1;
}
return result;
}
- 通过位运算进行左移1在于原值进行与计算
public static String toBinaryString1(int num) {
if (num == 0) return ""+0;
String result = "";
for (int i=Integer.SIZE -1; i >= 0; i--) {
int val = num & (1 << i);
int x = val > 0 || val ==Integer.MIN_VALUE? 1 : 0;
result += x;
}
return result;
}