散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存储存位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
一个引子
// 一个的字符串,里面只含有小写字母,如何统计每个字母出现的次数?
1. 我们首先创建一个数组
int[] list = new int[26];
2. 给每个字母(letter)在数组中设定一个:letter - 'a'
3. 遍历字符串,给相应下标的值+1
4. 代码如下:
public class Main {
public static void main(String[] args) {
String s = "aabbd";
int len = s.length();
int[] list = new int[26];
for (int i = 0; i < len; i++) {
char letter = s.charAt(i);
list[letter - 'a']++;
}
System.out.println(Arrays.toString(list));
}
}
// [2, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
这样实现延伸的两个问题
- 如果字符串里不只是字母,甚至可能是更复杂的对象呢。(我们需要一个更好的散列函数)
- 这只是一个普通的数组,虽然我们可以根据下标还原字母的值,但是不够通用。(我们考虑把字母和出现的次数合并在一起:key-value)
001、散列表
- 散列表是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
- 由定义我们可以知道,散列表用的是数组支持下标访问数据的特性,所以散列表是数组的一种扩展,由数组演化而来。
- 数组中实际存储的是一个指针,一般指向一个链表(在JDK1.8后,也可以是红黑树),链表(红黑树)里的节点就是一个个的key-value键值对(entry)。
010、散列函数
- 散列函数(也叫哈希函数)就是一个无论你输入什么数据,它都会还你一个数字的函数。
- 它必须是一致的。如果你第一次输入apple返回的是3,那么每次输入apple,这个散列函数都必须返回3。
- 散列函数应尽可能为每个输入映射到不同的数字。如果无论输入什么,都返回3的话,那它就不是一个好的散列函数。
引子中的 letter - ‘a’ 其实就是一个散列函数,它使用的方法是直接定址法。
直接定址法:取关键字或关键字的某个线性函数值为散列地址。
- 通过散列函数返回的值(hashcode),我们就可以得到一个下标,通常来说,他的下标是 hash值 % 数组容量。
- 通过下标,我们就可以找到key-value的地址,从而取得相应的值。
011、冲突
当散列函数不够好,很容易就造成哈希冲突,多个key返回相同的值。当然,再好的散列函数,都难以避免冲突的发生。
开放寻址法
- 当一个关键字和另一个关键字发生冲突时,使用某种探测技术在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中。
- 线性探测:当根据hash值查到的key和想要的不一样,那就继续往下寻找,最坏的情况会变成遍历一遍散列表。
- 此外还有二次探索、双重散列等
链表法(常用)
- 产生hash冲突后在存储数据后面加一个指针,指向后面冲突的数据(hashmap)
- 在散列表中每个下标位置对应一个链表,所有经过散列函数得到的散列值相同的元素,我们都放到对应下标位置的链表中。
- 查询的时候,先使用hash值找到下标,然后用equals找到正确的key对应的value。
还有一些比如公共溢出区法、再散列法等
100、扩容
再好的散列函数也可能出现hash冲突,除了解决冲突,还能通过增加容量来减少冲突的发生–负载因子,也叫填装因子。散列表的负载因子=散列表保存的元素数/位置总数。当散列表中元素的个数大于数组容量*负载因子时,就会触发散列表扩容。
- 通常严格限制在0.7-0.8以下,其中java的hashmap是0.75。
1. 在java中HashMap的长度是2的n次方,为什么?
- 计算机中取模(%)效率比位运算低。
- HashMap中一个key,他的下标是hash&(cap-1)
- 当容量cap为2的n次方,cap-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突
- 如果cap为2的n次方,那么我可以知道hash%cap==hash&(cap-1)
- 用位运算(hash&(length-1))替代模运算,提高效率
2. Java中HashMap负载因子为什么是0.75
- 一种可能的说法:如果 负载因子 是 3/4 的话,那么和 容量 的乘积结果就可以是一个整数。