哈希表的实现

哈希表的实现

1、TreeMap分析

  • 时间复杂度(平均)
    • 添加、删除、搜索:O(logn)
  • 特点
    • Key必须具备可比较性
    • 元素的分布是有顺序的
  • 在实际应用中,很多时候的需求
    • Map中存储的元素不需要讲究顺序
    • Map中的Key不需要具备可比较性
  • 不考虑顺序、不考虑Key的可比较性,Map有更好的实现方案,平均时间复杂度可以达到O(1)
    • 那就是采取哈希表来实现Map

2、哈希表(Hash Table)

  • 哈希表也叫作散列表
  • 它是如何实现高效处理数据的?
    • put("Jack",666)
    • put("Rose",777)
    • put("Kate",888)
  • 添加、搜索、删除的流程都是类似的
    • 利用哈希函数生成Key对应的index【O(1)】
    • 根据index操作定位数组元素【O(1)】
  • 哈希表是【空间换时间】的典型应用
  • 哈希函数,也叫做散列函数
  • 哈希表内部的数组元素,很多地方也叫Bucket(桶),整个数组叫Buckets或者Bucket Array
哈希表的实现

3、哈希冲突(Hash Collision)

  • 哈希冲突也叫做哈希碰撞
    • 2个不同的Key,经过哈希函数计算出相同的结果
    • Key1 != key2,hash(Key1) = hash(key2)
  • 解决哈希冲突的常见方式
    • 开放定址法(Open Addressing)
      • 按照一定规则向其他地址探测,知道遇到空桶
    • 再哈希法(Re-Hashing)
      • 设计多个哈希函数
    • 链地址法(Separate Chaining)
      • 比如通过链表将同一index的元素串起来

哈希表的实现

4、JDK1.8的哈希冲突解决方案

  • 默认使用单向链表将元素串起来
  • 在添加元素时,可能会由单向链表转为红黑树来存储元素
    • 比如当哈希表容量>=64且单向链表的节点数量大于8时
  • 当红黑树节点数量少到一定程度时,又会转为单向链表
  • JDK1.8中的哈希表是使用链表+红黑树解决哈希冲突
哈希表的实现

5、哈希函数

  • 哈希表中哈希函数的实现步骤大概如下

    • 1.先生成Key的哈希值(必须是整数)
    • 2.再让Key的哈希值跟数组的大小进行相关运算,生成一个索引值
    Public int hash(Object key){
    	return hash_code(key) % table.length;
    }
    
  • 为了提高效率,可以使用&位运算取代%运算【前提:将数组的长度设计为2的幂(2^n)】

    Public int hash(Object key){
    	return hash_code(key) & (table.length - 1);
    }
    
  • 良好的哈希函数

    • 让哈希表更加均匀分布-->减少哈希冲突次数-->提升哈希表的性能

6、如何生成Key的哈希值

  • key的常见类型种类可能有
    • 整数、浮点数、字符串、自定义对象
    • 不同种类的Key,哈希值的生成方式不一样,但目标是一致的
      • 尽量让每个key的哈希值是唯一的
      • 尽量让key的所有信息参与运算
  • 在Java中,HashMap的key必须实现hashCode、equals方法,也允许key为null
  • 整数
    • 整数值当做哈希值
    • 比如10的哈希值就是10
  • 浮点数
    • 将存储的二进制格式转为整数值
public static int hashCode(int value){
	return value;
}
public static int hashCode(float value){
	return floatToIntBits(value);
}

7、Long和Double的哈希值

public static int hashCode(long value){
	return (int)(value ^ (value >>> 32));
}
public static int hashCode(double value){
	long bits = doubleToLongBits(value);
	return (int)(bits ^ (bits >>> 32));
}

>>>和^的作用是?

  • 高32bit和低32bit混合计算出32bit的哈希值
  • 充分利用所有信息计算出哈希值

哈希表的实现

8、字符串的哈希表

  • 整数5489是如何计算出来的
    • 5 ∗ 103 + 4 ∗ 102 + 8 ∗ 101 + 9 ∗ 100
  • 字符串是由若干个字符组成的
    • 比如字符串 jack,由 j、a、c、k 四个字符组成(字符的本质就是一个整数)
    • 因此,jack 的哈希值可以表示为 j ∗ n 3 + a ∗ n 2 + c ∗ n 1 + k ∗ n 0,等价于 [ ( j ∗ n + a ) ∗ n + c ] ∗ n + k
    • 在JDK中,乘数 n 为 31,为什么使用 31?
    • 31 是一个奇素数,JVM会将 31 * i 优化成 (i << 5) – i
String string = "jack";
int hashCode = 0;
int len = string.length;
for(int i = 0; i < len; i++){
	char c = string.charAt(i);
	hashCode = 31 * hashCode + c;
}
String string = "jack";
int hashCode = 0;
int len = string.length;
for(int i = 0; i < len; i++){
	char c = string.charAt(i);
	hashCode = (hashCode << 5) - hasCode + c;
}

9、关于31的探讨

  • 31 * i = (2^5 – 1) * i = i * 2^5 – i = (i << 5) – i
  • 31不仅仅是符合2^n – 1,它是个奇素数(既是奇数,又是素数,也就是质数)
    • 素数和其他数相乘的结果比其他方式更容易产成唯一性,减少哈希冲突
    • 最终选择31是经过观测分布结果后的选择

10、自定义对象的哈希值

public class Person implements Comparable<Person> {
	private int age;   // 10  20
	private float height; // 1.55 1.67
	private String name; // "jack" "rose"
	
	public Person(int age, float height, String name) {
		this.age = age;
		this.height = height;
		this.name = name;
	}
	
	@Override
	/**
	 * 用来比较2个对象是否相等
	 */
	public boolean equals(Object obj) {
		// 内存地址
		if (this == obj) return true;
		if (obj == null || obj.getClass() != getClass()) return false;
		// if (obj == null || !(obj instanceof Person)) return false;
		
		// 比较成员变量
		Person person = (Person) obj;
		return person.age == age
				&& person.height == height
				&& (person.name == null ? name == null : person.name.equals(name));
	}
	
	@Override
	public int hashCode() {
		int hashCode = Integer.hashCode(age);
		hashCode = hashCode * 31 + Float.hashCode(height);
		hashCode = hashCode * 31 + (name != null ? name.hashCode() : 0);
		return hashCode;
	}

	@Override
	public int compareTo(Person o) {
		return age - o.age;
	}
}

11、自定义对象作为Key

  • 自定义对象作为 key,最好同时重写 hashCode 、equals 方法
    • equals :用以判断 2 个 key 是否为同一个 key
      • 自反性:对于任何非 null 的 x,x.equals(x)必须返回true
      • 对称性:对于任何非 null 的 x、y,如果 y.equals(x) 返回 true,x.equals(y) 必须返回 true
      • 传递性:对于任何非 null 的 x、y、z,如果 x.equals(y)、y.equals(z) 返回 true,那么x.equals(z) 必须 返回 true
      • 一致性:对于任何非 null 的 x、y,只要 equals 的比较操作在对象中所用的信息没有被修改,多次调用 x.equals(y) 就会一致地返回 true,或者一致地返回 false
      • 对于任何非 null 的 x,x.equals(null) 必须返回 false
    • hashCode :必须保证 equals 为 true 的 2 个 key 的哈希值一样
    • 反过来 hashCode 相等的 key,不一定 equals 为 true
  • 不重写 hashCode 方法只重写 equals 会有什么后果?
    • 可能会导致 2 个 equals 为 true 的 key 同时存在哈希表中

12、哈希值的进一步处理”扰动计算

private int hash(K key){
	if(key == null){
		return 0;
	}
	int h = key.hashCode();
	return (h ^ (h >>> 16)) & (table.length - 1);
}

13、装填因子

  • 装填因子(Load Factor):节点总数量 / 哈希表桶数组长度,也叫做负载因子
  • 在JDK1.8的HashMap中,如果装填因子超过0.75,就扩容为原来的2倍

14、TreeMap vs HashMap

  • 何时选择TreeMap?
    • 元素具备可比较性且要求升序遍历(按照元素从小到大)
  • 何时选择HashMap?
    • 无序遍历

15、LinkedHashMap

  • 在HashMap的基础上维护元素的添加顺序,使得遍历的结果是遵从添加顺序的
  • 删除度为2的节点node时
    • 需要注意更换 node 与 前驱\后继节点 的连接位置
  • 假设添加顺序是
    • 37、21、31、41、97、95、52、42、83
哈希表的实现

LinkedHashMap – 删除注意点

  • 删除度为2的节点node时(比如删除31)
    • 需要注意更换 node 与 前驱\后继节点 的连接位置

哈希表的实现

16、LinkedHashMap – 更换节点的连接位置

哈希表的实现

// 交换prev
LinkedNode<K, V> tmp = node1.prev;
node1.prev = node2.prev;
node2.prev = tmp;
if (node1.prev == null) {
	first = node1;
} else {
	node1.prev.next = node1;
}
if (node2.prev == null) {
	first = node2;
} else {
	node2.prev.next = node2;
}
// 交换next
tmp = node1.next;
node1.next = node2.next;
node2.next = tmp;
if (node1.next == null) {
	last = node1;
} else {
	node1.next.prev = node1;
}
if (node2.next == null) {
	last = node2;
} else {
	node2.next.prev = node2;
}

17、关于使用%来计算索引

  • 如果使用%来计算索引
    • 建议把哈希表的长度设计为素数(质数)
    • 可以大大减小哈希冲突
  • 右边表格列出了不同数据规模对应的最佳素数,特点如下
    • 每个素数略小于前一个素数的2倍
    • 每个素数尽可能接近2的幂(2 n)
哈希表的实现
上一篇:equals()重写之后为什么要重写hashCode()方法


下一篇:6.原型模式prototype