文章目录
- 一、解决哈希冲突
- 1.1闭散列
- 1.1.1线性探测
- 1.1.2二次探测
- 1.2开散列
- 二、模拟实现哈希表
- 三、HashMap源码的一些相关内容
哈希(散列)方法:构造一种存储结构,通过某种函数使元素的存储位置与它的关键码之间能够建立 一 一 映射关系,那么在查找时通过该函数就可以很快找到该元素。
哈希(散列)方法中的函数称为哈希(散列)函数,构造出来的结构称为哈希表(散列表)。
例如在集合{1,2,3,4,5}中,哈希函数设置为hash(key)=key%capacity。
哈希函数的选择和数据整体元素有关,与单个元素没有直接关联。
对于哈希冲突,避免冲突的方法有:①设置合理的哈希函数 ②降低负载因子(即提高散列表的长度),负载因子=填入表中的元素个数/散列表的长度;解决冲突的方法有:①闭散列 ②开散列
采用哈希处理,一般所需空间都会比元素个数多,否则产生冲突的概率就比较大,所以哈希表相当于用空间来换取时间,哈希表的时间复杂度是O(1)。搜索时二叉搜索树比哈希表效率低,二叉搜索树的时间复杂度为O(logN)或O(N),哈希表的时间复杂度为O(1)。
hashCode()可以将对象转变为一个整数(为这个对象返回一个哈希值,hashCode()被支持使用到哈希表当中)。
一、解决哈希冲突
1.1闭散列
闭散列法,也叫开放定址法,发生哈希冲突时,如果哈希表未被装满,则把key存放到冲突位置的“下一个”空位置中去。
1.1.1线性探测
线性探测找“下一个”空位置:从发生冲突的位置开始,依次向后探测“下一个”空位置。
线性探测的缺点:
①把更多冲突的元素聚集在一起,这与其找下一个空位置有关
②不能随便删除哈希表1中已有的元素,如删除5,则55查找起来可能会受影响
1.1.2二次探测
为了避免把更多冲突的元素聚集在一起,二次探测找“下一个”空位置的方法为:Hi = (H0 + i ^2) % capacity 或 Hi = (H0 - i ^2) % capacity
与线性探测相比二次探测中冲突的元素较分开。
闭散列的缺点:空间利用率较低,也是哈希的缺陷。
1.2开散列
开散列法,也叫链地址法/开链法,使用哈希桶方式解决哈希冲突。哈希桶方式解决哈希冲突:哈希表是数组+链表的结构,当链表的长度超过8 && 数组的长度超过64时,链表变成红黑树。向哈希表中插入元素时JDK1.8之前是头插法,JDK1.8之后是尾插法。
二、模拟实现哈希表
这里用了开散列法解决哈希冲突。
//key-value模型
class Person {public String id;public Person(String id) {this.id = id;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return Objects.equals(id, person.id);}@Overridepublic int hashCode() {return Objects.hash(id);}
}
public class HashBucket<K, V> {private static class Node<K, V> {private K key;private V val;Node next;public Node(K key, V value) {this.key = key;this.val = value;}}private Node<K, V>[] array;private int usedSize;private static final double LOAD_FACTOR = 0.75;private static final int DEFAULT_SIZE = 8;//默认桶的大小public HashBucket() {array = (Node<K, V>[])new Node[10];}public void put(K key, V val) {int hash = key.hashCode();int index = hash % array.length;Node<K, V> cur = array[index];while(cur != null) {if(cur.key.equals(key)) {cur.val = val;return;}cur = cur.next;}Node<K, V> node = new Node<>(key, val);//头插法node.next = array[index];array[index] = node;usedSize++;if(loadFactor() >= LOAD_FACTOR) {resize();}}//扩容要把所有的元素重新进行哈希private void resize() {Node<K, V>[] tmpArr = new Node[array.length * 2];//遍历原来数组,将原来数组的元素重新哈希到新的数组当中。因为要遍历原来的数组,所以扩容时要申请一个新的数组for (int i = 0; i < array.length; i++) {Node<K, V> cur = array[i];while(cur != null) {Node<K, V> curNext = cur.next;int hash = cur.key.hashCode();int newIndex = hash % array.length;//头插法cur.next = tmpArr[newIndex];tmpArr[newIndex] = cur;cur = curNext;}}array = tmpArr;}private double loadFactor() {return usedSize * 1.0 / array.length;//散列表的载荷因子=填入表中的元素个数/散列表的长度}public V get(K key) {int hash = key.hashCode();int index = hash % array.length;Node<K, V> cur = array[index];while (cur != null) {if (cur.key.equals(key)) {return cur.val;}}return null;}
}
三、HashMap源码的一些相关内容
HashMap的其中一个构造方法HashMap(int initialCapacity, float loadFactor),这个构造方法里面有一个tableSizeFor(int cap)方法,tableSizeFor(int cap)方法的作用是返回一个接近目标容量的二次幂,如HashMap(int initialCapacity, float loadFactor)中的initialCapacity给了1000,则tableSizeFor(int cap)返回1024(返回大于1000的不返回小于1000的)。所以实例化HashMap时,initialCapacity给了1000,最后数组容量则是1024。
(h = key.hashCode()) ^ (h >>> 16)的目的是使关键字在哈希表中尽可能更均匀地分布。putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)中有(n - 1) & hash,n是数组长度,当(n - 1)的值比较小时,(n - 1)和hash二进制序列参与到计算中的只有低位。多个hash和(n - 1)进行计算,如果hash和(n - 1)这两者的值的二进制序列均是低位相同,高位不同的话,(n - 1) & hash计算出来的数组下标都是同一个,增加了冲突的几率,所以要用(h = key.hashCode()) ^ (h >>> 16)计算hash。当n是二次幂时,hash%n和hash&(n-1)的结果一样。&的结果使二进制序列更向0集中,|的结果使二进制序列更向1集中, ^的结果使二进制序列更加倾向保留参与计算的两者的二进制序列各自的特征。(h = key.hashCode()) ^ (h >>> 16)使hash的低位二进制序列既保留hashCode()二进制序列高位的特征,又保留了hashCode()二进制序列低位的特征。