哈希表
数据结构:哈希表
哈希表:通过关键码来映射到值的一个数据结构。
- 哈希函数:键与值映射的一个映射关系;
哈希:
Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
所有散列函数都有如下一个基本特性:
根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。
但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。
碰撞:
两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫做碰撞。又叫“冲突”,哈希冲突:不同的元素通过同一个哈希函数插入到同一个位置;
m != n ====> f(m) = f(n)。
常见的Hash函数有以下几个:
直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。f(X) = kx + b (k 、b 都是常数);查询和添加都应对应同一个哈希函数,插入的顺序和存放的顺序不一致,键无序。
数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。
除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。 f(X) = x % k (k <= m)[m 为存储位置长度];
分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。
平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。
伪随机数法:采用一个伪随机数当作哈希函数。
如何解决哈希冲突??
衡量一个哈希函数的好坏的重要指标就是发生碰撞的概率以及发生碰撞的解决方案。
任何哈希函数基本都无法彻底避免碰撞,常见的解决碰撞的方法有以下几种:
-
1.开放定址法:
- 开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
-
2.链地址法:
- 将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
- 每一个索引位置存储的数据以链表的形式连接,当访问查找数据时,对相应索引位置的链表进行遍历。
- ( 数组 + 链表)
-
3.再哈希法:
- 当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。
-
4.建立公共溢出区:
- 将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。
-
5.探测法;线性探测;随机探测(看图解)
HashMap中的应用
hash方法的功能是根据Key来定位这个K-V在链表数组中的位置的。
也就是hash方法的输入应该是个Object类型的Key,输出应该是个int类型的数组下标。
//该方法主要是将Object转化成一个整型
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();//异或
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
//主要将hash生成的整型转化为链表数组中的下标
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);//取模 & 运算符效率高
}
length 16
1001 1111 0110 0110
0000 0000 0000 1111
-------------------
0000 0000 0000 0110
==>6
取模。Java之所有使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。
那么,为什么可以使用位运算(&)来实现取模运算(%)呢?这实现的原理如下:
X % 2^n = X & (2^n – 1)
2n表示2的n次方,也就是说,一个数对2n取模 == 一个数和(2^n – 1)做按位与运算 。
假设n为3,则2^3 = 8,表示成2进制就是1000。2^3 = 7 ,即0111。
此时X & (2^3 – 1) 就相当于取X的2进制的最后三位数。
从2进制角度来看,X / 8相当于 X >> 3,即把X右移3位,此时得到了X / 8的商,而被移掉的部分(后三位),则是X % 8,也就是余数。
上面的解释不知道你有没有看懂,没看懂的话其实也没关系,你只需要记住这个技巧就可以了。或者你可以找几个例子试一下。
6 % 8 = 6 ,6 & 7 = 6
10 & 8 = 2 ,10 & 7 = 2
所以,return h & (length-1);
只要保证length的长度是2^n
的话,就可以实现取模运算了。而HashMap中的length也确实是2的倍数,初始值是16,之后每次扩充为原来的2倍。